showpane 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/bundle/meta/scaffold-manifest.json +73 -0
- package/bundle/scaffold/VERSION +1 -0
- package/bundle/scaffold/__dot__env.example +24 -0
- package/bundle/scaffold/__dot__gitignore +41 -0
- package/bundle/scaffold/docker/Caddyfile +3 -0
- package/bundle/scaffold/docker/Dockerfile +30 -0
- package/bundle/scaffold/docker-compose.yml +53 -0
- package/bundle/scaffold/next.config.ts +20 -0
- package/bundle/scaffold/package-lock.json +5843 -0
- package/bundle/scaffold/package.json +42 -0
- package/bundle/scaffold/postcss.config.js +6 -0
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
- package/bundle/scaffold/prisma/schema.local.prisma +131 -0
- package/bundle/scaffold/prisma/schema.prisma +128 -0
- package/bundle/scaffold/prisma/seed.ts +49 -0
- package/bundle/scaffold/public/example-avatar.svg +4 -0
- package/bundle/scaffold/public/example-logo.svg +4 -0
- package/bundle/scaffold/public/robots.txt +2 -0
- package/bundle/scaffold/scripts/backup.sh +19 -0
- package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
- package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
- package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
- package/bundle/scaffold/scripts/restore.sh +31 -0
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
- package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
- package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
- package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
- package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
- package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
- package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
- package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
- package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
- package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
- package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
- package/bundle/scaffold/src/app/api/health/route.ts +19 -0
- package/bundle/scaffold/src/app/globals.css +7 -0
- package/bundle/scaffold/src/app/layout.tsx +25 -0
- package/bundle/scaffold/src/app/page.tsx +171 -0
- package/bundle/scaffold/src/components/portal-login.tsx +169 -0
- package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
- package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
- package/bundle/scaffold/src/lib/branding.ts +50 -0
- package/bundle/scaffold/src/lib/client-auth.ts +98 -0
- package/bundle/scaffold/src/lib/client-portals.ts +134 -0
- package/bundle/scaffold/src/lib/control-plane.ts +100 -0
- package/bundle/scaffold/src/lib/db.ts +7 -0
- package/bundle/scaffold/src/lib/files.ts +124 -0
- package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
- package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
- package/bundle/scaffold/src/lib/storage.ts +204 -0
- package/bundle/scaffold/src/lib/token.ts +186 -0
- package/bundle/scaffold/src/lib/utils.ts +6 -0
- package/bundle/scaffold/src/middleware.ts +61 -0
- package/bundle/scaffold/tailwind.config.ts +15 -0
- package/bundle/scaffold/tests/__dot__gitkeep +0 -0
- package/bundle/scaffold/tsconfig.json +23 -0
- package/bundle/scaffold/vitest.config.ts +13 -0
- package/bundle/toolchain/VERSION +1 -0
- package/bundle/toolchain/bin/check-slug.ts +59 -0
- package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
- package/bundle/toolchain/bin/create-portal.ts +71 -0
- package/bundle/toolchain/bin/delete-portal.ts +48 -0
- package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
- package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
- package/bundle/toolchain/bin/generate-share-link.ts +68 -0
- package/bundle/toolchain/bin/list-portals.ts +53 -0
- package/bundle/toolchain/bin/materialize-file.ts +35 -0
- package/bundle/toolchain/bin/query-analytics.ts +88 -0
- package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
- package/bundle/toolchain/bin/showpane-config +63 -0
- package/bundle/toolchain/bin/tsconfig.json +13 -0
- package/bundle/toolchain/skills/VERSION +1 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
- package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
- package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
- package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
- package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
- package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
- package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
- package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
- package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
- package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
- package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
- package/bundle/toolchain/skills/shared/preamble.md +137 -0
- package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
- package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
- package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
- package/dist/index.js +873 -166
- package/package.json +3 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
|
|
3
|
+
function fail(message: string): never {
|
|
4
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Args = {
|
|
9
|
+
orgId?: string;
|
|
10
|
+
orgSlug?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv: string[]): Args {
|
|
14
|
+
const getArg = (flag: string) => {
|
|
15
|
+
const index = argv.indexOf(flag);
|
|
16
|
+
return index !== -1 ? argv[index + 1] : undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
orgId: getArg("--org-id"),
|
|
21
|
+
orgSlug: getArg("--org-slug"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
if (args.includes("--help")) {
|
|
29
|
+
console.log("Usage: export-runtime-state [--org-id <orgId>] [--org-slug <slug>]");
|
|
30
|
+
console.log("Exports the local portal runtime state for cloud deploy sync.");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { orgId, orgSlug } = parseArgs(args);
|
|
35
|
+
const prisma = new PrismaClient();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const organization = orgId
|
|
39
|
+
? await prisma.organization.findUnique({ where: { id: orgId } })
|
|
40
|
+
: orgSlug
|
|
41
|
+
? await prisma.organization.findUnique({ where: { slug: orgSlug } })
|
|
42
|
+
: await prisma.organization.findFirst({ orderBy: { createdAt: "asc" } });
|
|
43
|
+
|
|
44
|
+
if (!organization) {
|
|
45
|
+
fail("No organization found");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const portals = await prisma.clientPortal.findMany({
|
|
49
|
+
where: { organizationId: organization.id },
|
|
50
|
+
orderBy: { createdAt: "asc" },
|
|
51
|
+
select: {
|
|
52
|
+
slug: true,
|
|
53
|
+
companyName: true,
|
|
54
|
+
logoUrl: true,
|
|
55
|
+
username: true,
|
|
56
|
+
passwordHash: true,
|
|
57
|
+
credentialVersion: true,
|
|
58
|
+
isActive: true,
|
|
59
|
+
lastUpdated: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
ok: true,
|
|
65
|
+
organization: {
|
|
66
|
+
id: organization.id,
|
|
67
|
+
slug: organization.slug,
|
|
68
|
+
name: organization.name,
|
|
69
|
+
logoUrl: organization.logoUrl,
|
|
70
|
+
primaryColor: organization.primaryColor,
|
|
71
|
+
portalLabel: organization.portalLabel,
|
|
72
|
+
websiteUrl: organization.websiteUrl,
|
|
73
|
+
contactName: organization.contactName,
|
|
74
|
+
contactTitle: organization.contactTitle,
|
|
75
|
+
contactEmail: organization.contactEmail,
|
|
76
|
+
contactPhone: organization.contactPhone,
|
|
77
|
+
contactAvatar: organization.contactAvatar,
|
|
78
|
+
supportEmail: organization.supportEmail,
|
|
79
|
+
customDomain: organization.customDomain,
|
|
80
|
+
},
|
|
81
|
+
portals,
|
|
82
|
+
}));
|
|
83
|
+
} finally {
|
|
84
|
+
await prisma.$disconnect();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main().catch((error) => {
|
|
89
|
+
fail(String(error));
|
|
90
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
import { buildAndSignToken, SHARE_TOKEN_MAX_AGE_SECONDS } from "@/lib/token";
|
|
3
|
+
|
|
4
|
+
function fail(message: string): never {
|
|
5
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getArg(args: string[], flag: string): string | undefined {
|
|
10
|
+
const idx = args.indexOf(flag);
|
|
11
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
if (args.includes("--help")) {
|
|
18
|
+
console.log("Usage: generate-share-link --slug <slug> --org-id <orgId> [--base-url <url>]");
|
|
19
|
+
console.log("Generates a reusable share link for a portal.");
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const slug = getArg(args, "--slug");
|
|
24
|
+
const orgId = getArg(args, "--org-id");
|
|
25
|
+
const baseUrl = getArg(args, "--base-url")
|
|
26
|
+
?? process.env.NEXT_PUBLIC_APP_URL
|
|
27
|
+
?? "http://localhost:3000";
|
|
28
|
+
|
|
29
|
+
if (!slug || !orgId) fail("Missing --slug or --org-id");
|
|
30
|
+
|
|
31
|
+
if (!process.env.AUTH_SECRET) fail("AUTH_SECRET not set in environment");
|
|
32
|
+
|
|
33
|
+
const prisma = new PrismaClient();
|
|
34
|
+
try {
|
|
35
|
+
const portal = await prisma.clientPortal.findUnique({
|
|
36
|
+
where: { organizationId_slug: { organizationId: orgId, slug } },
|
|
37
|
+
select: { credentialVersion: true, isActive: true },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!portal) fail(`Portal "${slug}" not found`);
|
|
41
|
+
if (!portal.isActive) fail(`Portal "${slug}" is inactive`);
|
|
42
|
+
|
|
43
|
+
const token = await buildAndSignToken(
|
|
44
|
+
orgId,
|
|
45
|
+
slug,
|
|
46
|
+
"share",
|
|
47
|
+
SHARE_TOKEN_MAX_AGE_SECONDS,
|
|
48
|
+
portal.credentialVersion
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!token) fail("Failed to sign token — check AUTH_SECRET");
|
|
52
|
+
|
|
53
|
+
const shareUrl = `${baseUrl.replace(/\/$/, "")}/client/${slug}/s/${token}`;
|
|
54
|
+
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
ok: true,
|
|
57
|
+
shareUrl,
|
|
58
|
+
expiresIn: "never",
|
|
59
|
+
}));
|
|
60
|
+
} finally {
|
|
61
|
+
await prisma.$disconnect();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
main().catch((e) => {
|
|
66
|
+
console.error(JSON.stringify({ ok: false, error: String(e) }));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
|
|
3
|
+
function fail(message: string): never {
|
|
4
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
if (args.includes("--help")) {
|
|
12
|
+
console.log("Usage: list-portals [--org-id <orgId>]");
|
|
13
|
+
console.log("Lists all client portals. Defaults to first organization if --org-id omitted.");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const prisma = new PrismaClient();
|
|
18
|
+
try {
|
|
19
|
+
const getArg = (flag: string) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : undefined; };
|
|
20
|
+
let orgId = getArg("--org-id");
|
|
21
|
+
|
|
22
|
+
if (!orgId) {
|
|
23
|
+
const firstOrg = await prisma.organization.findFirst({ select: { id: true } });
|
|
24
|
+
if (!firstOrg) fail("No organizations found");
|
|
25
|
+
orgId = firstOrg.id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const portals = await prisma.clientPortal.findMany({
|
|
29
|
+
where: { organizationId: orgId },
|
|
30
|
+
include: { organization: { select: { name: true } } },
|
|
31
|
+
orderBy: { createdAt: "desc" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
ok: true,
|
|
36
|
+
portals: portals.map((p) => ({
|
|
37
|
+
slug: p.slug,
|
|
38
|
+
companyName: p.companyName,
|
|
39
|
+
isActive: p.isActive,
|
|
40
|
+
lastUpdated: p.lastUpdated,
|
|
41
|
+
username: p.username,
|
|
42
|
+
createdAt: p.createdAt.toISOString(),
|
|
43
|
+
})),
|
|
44
|
+
}));
|
|
45
|
+
} finally {
|
|
46
|
+
await prisma.$disconnect();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((e) => {
|
|
51
|
+
console.error(JSON.stringify({ ok: false, error: String(e) }));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile_ } from "@/lib/storage";
|
|
4
|
+
|
|
5
|
+
function fail(message: string): never {
|
|
6
|
+
console.error(JSON.stringify({ error: message }));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const getArg = (flag: string) => {
|
|
13
|
+
const index = args.indexOf(flag);
|
|
14
|
+
return index !== -1 ? args[index + 1] : undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const storagePath = getArg("--storage-path");
|
|
18
|
+
const output = getArg("--output");
|
|
19
|
+
|
|
20
|
+
if (!storagePath || !output) {
|
|
21
|
+
fail("Missing --storage-path or --output");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = await readFile_(storagePath);
|
|
25
|
+
if (!data) {
|
|
26
|
+
fail(`File not found in storage: ${storagePath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
30
|
+
await writeFile(output, data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch((error) => {
|
|
34
|
+
fail(String(error));
|
|
35
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
|
|
3
|
+
function fail(message: string): never {
|
|
4
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getArg(args: string[], flag: string): string | undefined {
|
|
9
|
+
const idx = args.indexOf(flag);
|
|
10
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
|
|
16
|
+
if (args.includes("--help")) {
|
|
17
|
+
console.log("Usage: query-analytics [--slug <slug>] [--days <n>] --org-id <orgId>");
|
|
18
|
+
console.log("Queries portal events. Omit --slug for all portals. Default --days is 30.");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const slug = getArg(args, "--slug");
|
|
23
|
+
const days = parseInt(getArg(args, "--days") ?? "30", 10);
|
|
24
|
+
const orgId = getArg(args, "--org-id");
|
|
25
|
+
|
|
26
|
+
if (!orgId) fail("Missing --org-id");
|
|
27
|
+
|
|
28
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
29
|
+
const prisma = new PrismaClient();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Find portals in scope
|
|
33
|
+
const portals = await prisma.clientPortal.findMany({
|
|
34
|
+
where: {
|
|
35
|
+
organizationId: orgId,
|
|
36
|
+
...(slug ? { slug } : {}),
|
|
37
|
+
},
|
|
38
|
+
select: { id: true, slug: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (slug && portals.length === 0) fail(`Portal "${slug}" not found`);
|
|
42
|
+
|
|
43
|
+
const portalIds = portals.map((p) => p.id);
|
|
44
|
+
|
|
45
|
+
// Query events grouped by type
|
|
46
|
+
const events = await prisma.portalEvent.groupBy({
|
|
47
|
+
by: ["event", "portalId"],
|
|
48
|
+
where: {
|
|
49
|
+
portalId: { in: portalIds },
|
|
50
|
+
createdAt: { gte: since },
|
|
51
|
+
},
|
|
52
|
+
_count: { id: true },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get last activity
|
|
56
|
+
const lastEvent = await prisma.portalEvent.findFirst({
|
|
57
|
+
where: {
|
|
58
|
+
portalId: { in: portalIds },
|
|
59
|
+
createdAt: { gte: since },
|
|
60
|
+
},
|
|
61
|
+
orderBy: { createdAt: "desc" },
|
|
62
|
+
select: { createdAt: true },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Aggregate counts by event type
|
|
66
|
+
const counts: Record<string, number> = {};
|
|
67
|
+
for (const e of events) {
|
|
68
|
+
counts[e.event] = (counts[e.event] ?? 0) + e._count.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(JSON.stringify({
|
|
72
|
+
ok: true,
|
|
73
|
+
slug: slug ?? "all",
|
|
74
|
+
period: `${days}d`,
|
|
75
|
+
views: counts["portal_view"] ?? 0,
|
|
76
|
+
tabSwitches: counts["tab_switch"] ?? 0,
|
|
77
|
+
events: counts,
|
|
78
|
+
lastActivity: lastEvent?.createdAt.toISOString() ?? null,
|
|
79
|
+
}));
|
|
80
|
+
} finally {
|
|
81
|
+
await prisma.$disconnect();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((e) => {
|
|
86
|
+
console.error(JSON.stringify({ ok: false, error: String(e) }));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
|
|
5
|
+
function fail(message: string): never {
|
|
6
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
if (args.includes("--help")) {
|
|
14
|
+
console.log("Usage: rotate-credentials --slug <slug> --org-id <orgId>");
|
|
15
|
+
console.log("Rotates the password for a portal. New password shown once.");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const getArg = (flag: string) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : undefined; };
|
|
20
|
+
const slug = getArg("--slug");
|
|
21
|
+
const orgId = getArg("--org-id");
|
|
22
|
+
|
|
23
|
+
if (!slug || !orgId) fail("Missing --slug or --org-id");
|
|
24
|
+
|
|
25
|
+
const prisma = new PrismaClient();
|
|
26
|
+
try {
|
|
27
|
+
const portal = await prisma.clientPortal.findUnique({
|
|
28
|
+
where: { organizationId_slug: { organizationId: orgId, slug } },
|
|
29
|
+
});
|
|
30
|
+
if (!portal) fail(`Portal "${slug}" not found`);
|
|
31
|
+
|
|
32
|
+
const password = crypto.randomBytes(16).toString("base64url");
|
|
33
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
34
|
+
|
|
35
|
+
await prisma.clientPortal.update({
|
|
36
|
+
where: { id: portal.id },
|
|
37
|
+
data: {
|
|
38
|
+
passwordHash,
|
|
39
|
+
credentialVersion: crypto.randomUUID(),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(JSON.stringify({
|
|
44
|
+
ok: true,
|
|
45
|
+
username: portal.username,
|
|
46
|
+
password,
|
|
47
|
+
rotated: true,
|
|
48
|
+
}));
|
|
49
|
+
} finally {
|
|
50
|
+
await prisma.$disconnect();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch((e) => {
|
|
55
|
+
console.error(JSON.stringify({ ok: false, error: String(e) }));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
CONFIG_DIR="${HOME}/.showpane"
|
|
5
|
+
CONFIG_FILE="${CONFIG_DIR}/config.json"
|
|
6
|
+
|
|
7
|
+
usage() {
|
|
8
|
+
echo "Usage: showpane-config <get|set> <key> [value]"
|
|
9
|
+
echo ""
|
|
10
|
+
echo "Commands:"
|
|
11
|
+
echo " get <key> Read a value from config"
|
|
12
|
+
echo " set <key> <value> Write a value to config"
|
|
13
|
+
echo ""
|
|
14
|
+
echo "Config file: ${CONFIG_FILE}"
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ensure_config() {
|
|
19
|
+
if [ ! -d "$CONFIG_DIR" ]; then
|
|
20
|
+
mkdir -p "$CONFIG_DIR"
|
|
21
|
+
fi
|
|
22
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
23
|
+
echo '{}' > "$CONFIG_FILE"
|
|
24
|
+
chmod 600 "$CONFIG_FILE"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case "${1:-}" in
|
|
29
|
+
get)
|
|
30
|
+
[ -z "${2:-}" ] && usage
|
|
31
|
+
ensure_config
|
|
32
|
+
# Use python3 for portable JSON parsing (available on macOS + Linux)
|
|
33
|
+
python3 -c "
|
|
34
|
+
import json, sys
|
|
35
|
+
with open('$CONFIG_FILE') as f:
|
|
36
|
+
data = json.load(f)
|
|
37
|
+
key = '$2'
|
|
38
|
+
if key in data:
|
|
39
|
+
print(data[key])
|
|
40
|
+
else:
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
"
|
|
43
|
+
;;
|
|
44
|
+
set)
|
|
45
|
+
[ -z "${2:-}" ] || [ -z "${3:-}" ] && usage
|
|
46
|
+
ensure_config
|
|
47
|
+
python3 -c "
|
|
48
|
+
import json
|
|
49
|
+
with open('$CONFIG_FILE') as f:
|
|
50
|
+
data = json.load(f)
|
|
51
|
+
data['$2'] = '$3'
|
|
52
|
+
with open('$CONFIG_FILE', 'w') as f:
|
|
53
|
+
json.dump(data, f, indent=2)
|
|
54
|
+
"
|
|
55
|
+
chmod 600 "$CONFIG_FILE"
|
|
56
|
+
;;
|
|
57
|
+
--help|-h)
|
|
58
|
+
usage
|
|
59
|
+
;;
|
|
60
|
+
*)
|
|
61
|
+
usage
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.1.0 (requires app >= 0.2.0)
|