showpane 0.4.9 → 0.4.11
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/bundle/meta/scaffold-manifest.json +17 -24
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/__dot__env.example +3 -5
- package/bundle/scaffold/package.json +2 -1
- package/bundle/scaffold/prisma/schema.local.prisma +1 -1
- package/bundle/scaffold/prisma/seed.ts +6 -1
- package/bundle/scaffold/prisma.config.ts +5 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +1 -53
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +4 -37
- package/bundle/scaffold/src/__tests__/deploy-bundle.test.ts +48 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +1 -1
- package/bundle/scaffold/src/lib/client-portals.ts +8 -13
- package/bundle/scaffold/src/lib/deploy-bundle.ts +106 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +2 -32
- package/bundle/scaffold/src/types/adm-zip.d.ts +15 -0
- package/bundle/toolchain/VERSION +1 -1
- package/bundle/toolchain/bin/create-deploy-bundle.ts +3 -72
- package/bundle/toolchain/bin/ensure-cloud-project-link.ts +73 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-create/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +3 -3
- package/bundle/toolchain/skills/portal-delete/SKILL.md +4 -4
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +32 -264
- package/bundle/toolchain/skills/portal-dev/SKILL.md +15 -13
- package/bundle/toolchain/skills/portal-list/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-preview/SKILL.md +9 -23
- package/bundle/toolchain/skills/portal-setup/SKILL.md +19 -28
- package/bundle/toolchain/skills/portal-share/SKILL.md +3 -4
- package/bundle/toolchain/skills/portal-status/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-update/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-verify/SKILL.md +21 -33
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +1 -5
- package/bundle/toolchain/skills/shared/preamble.md +2 -2
- package/dist/index.js +56 -10
- package/package.json +1 -1
- package/bundle/scaffold/docker/Caddyfile +0 -3
- package/bundle/scaffold/docker/Dockerfile +0 -30
- package/bundle/scaffold/docker-compose.yml +0 -53
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +0 -143
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +0 -6
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +0 -2
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +0 -3
- package/bundle/scaffold/prisma/schema.prisma +0 -128
- package/bundle/scaffold/scripts/backup.sh +0 -19
- package/bundle/scaffold/scripts/e2e-verify.sh +0 -487
- package/bundle/scaffold/scripts/restore.sh +0 -31
package/dist/index.js
CHANGED
|
@@ -487,13 +487,27 @@ function readShowpaneConfig() {
|
|
|
487
487
|
function writeShowpaneConfig(config) {
|
|
488
488
|
ensureDir(SHOWPANE_HOME);
|
|
489
489
|
const configPath = getShowpaneConfigPath();
|
|
490
|
-
|
|
490
|
+
const normalizedConfig = {
|
|
491
|
+
...config,
|
|
492
|
+
deploy_mode: typeof config.deploy_mode === "string" ? normalizeDeployMode(config.deploy_mode) : config.deploy_mode,
|
|
493
|
+
workspaces: config.workspaces?.map((workspace) => ({
|
|
494
|
+
...workspace,
|
|
495
|
+
deployMode: normalizeDeployMode(workspace.deployMode)
|
|
496
|
+
}))
|
|
497
|
+
};
|
|
498
|
+
writeJson(configPath, normalizedConfig);
|
|
491
499
|
chmodSync(configPath, 384);
|
|
492
500
|
}
|
|
501
|
+
function normalizeDeployMode(mode) {
|
|
502
|
+
return mode === "cloud" ? "cloud" : "local";
|
|
503
|
+
}
|
|
504
|
+
function hasShowpaneProjectShape(projectPath) {
|
|
505
|
+
return existsSync(join(projectPath, "package.json")) && (existsSync(join(projectPath, "prisma.config.ts")) || existsSync(join(projectPath, "prisma", "schema.local.prisma")));
|
|
506
|
+
}
|
|
493
507
|
function findWorkspaceRoot(startPath) {
|
|
494
508
|
let currentPath = resolve(startPath);
|
|
495
509
|
while (true) {
|
|
496
|
-
if (
|
|
510
|
+
if (hasShowpaneProjectShape(currentPath) && existsSync(getProjectMetadataPath(currentPath))) {
|
|
497
511
|
return currentPath;
|
|
498
512
|
}
|
|
499
513
|
const parentPath = dirname(currentPath);
|
|
@@ -504,7 +518,7 @@ function findWorkspaceRoot(startPath) {
|
|
|
504
518
|
}
|
|
505
519
|
}
|
|
506
520
|
function defaultWorkspaceEntry(projectPath, overrides) {
|
|
507
|
-
|
|
521
|
+
const workspace = {
|
|
508
522
|
name: basename(projectPath),
|
|
509
523
|
path: resolve(projectPath),
|
|
510
524
|
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -512,13 +526,15 @@ function defaultWorkspaceEntry(projectPath, overrides) {
|
|
|
512
526
|
orgSlug: "",
|
|
513
527
|
...overrides
|
|
514
528
|
};
|
|
529
|
+
workspace.deployMode = normalizeDeployMode(workspace.deployMode);
|
|
530
|
+
return workspace;
|
|
515
531
|
}
|
|
516
532
|
function getWorkspaceEntries(config) {
|
|
517
533
|
const workspaces = [...config.workspaces ?? []];
|
|
518
534
|
const activePath = config.app_path ? resolve(config.app_path) : null;
|
|
519
535
|
if (activePath && !workspaces.some((workspace) => normalizePathForComparison(workspace.path) === normalizePathForComparison(activePath))) {
|
|
520
536
|
workspaces.push(defaultWorkspaceEntry(activePath, {
|
|
521
|
-
deployMode:
|
|
537
|
+
deployMode: normalizeDeployMode(config.deploy_mode),
|
|
522
538
|
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : ""
|
|
523
539
|
}));
|
|
524
540
|
}
|
|
@@ -526,13 +542,13 @@ function getWorkspaceEntries(config) {
|
|
|
526
542
|
...workspace,
|
|
527
543
|
path: resolve(workspace.path),
|
|
528
544
|
lastUsedAt: workspace.lastUsedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
529
|
-
deployMode: workspace.deployMode
|
|
545
|
+
deployMode: normalizeDeployMode(workspace.deployMode),
|
|
530
546
|
orgSlug: workspace.orgSlug || ""
|
|
531
547
|
})).sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
532
548
|
}
|
|
533
549
|
function setActiveWorkspace(config, workspace) {
|
|
534
550
|
config.app_path = workspace.path;
|
|
535
|
-
config.deploy_mode = workspace.deployMode;
|
|
551
|
+
config.deploy_mode = normalizeDeployMode(workspace.deployMode);
|
|
536
552
|
config.orgSlug = workspace.orgSlug;
|
|
537
553
|
}
|
|
538
554
|
function upsertWorkspace(config, workspace, makeActive = true) {
|
|
@@ -547,7 +563,7 @@ function upsertWorkspace(config, workspace, makeActive = true) {
|
|
|
547
563
|
}
|
|
548
564
|
function updateWorkspaceFromConfig(config, projectPath, overrides) {
|
|
549
565
|
const workspace = defaultWorkspaceEntry(projectPath, {
|
|
550
|
-
deployMode:
|
|
566
|
+
deployMode: normalizeDeployMode(config.deploy_mode),
|
|
551
567
|
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : "",
|
|
552
568
|
...overrides
|
|
553
569
|
});
|
|
@@ -572,6 +588,27 @@ function ensureShowpaneShim() {
|
|
|
572
588
|
function ensureDir(dirPath) {
|
|
573
589
|
mkdirSync(dirPath, { recursive: true });
|
|
574
590
|
}
|
|
591
|
+
async function fetchCloudProjectLink(accessToken) {
|
|
592
|
+
const res = await fetch(`${API_BASE}/api/cli/project-link`, {
|
|
593
|
+
headers: {
|
|
594
|
+
Authorization: `Bearer ${accessToken}`
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (!res.ok) {
|
|
598
|
+
const body = await res.text();
|
|
599
|
+
throw new Error(`Could not fetch cloud project link (${res.status}): ${body}`);
|
|
600
|
+
}
|
|
601
|
+
return res.json();
|
|
602
|
+
}
|
|
603
|
+
function writeCloudProjectLink(projectRoot, projectLink) {
|
|
604
|
+
const vercelDir = join(projectRoot, ".vercel");
|
|
605
|
+
ensureDir(vercelDir);
|
|
606
|
+
writeJson(join(vercelDir, "project.json"), projectLink);
|
|
607
|
+
}
|
|
608
|
+
async function syncCloudProjectLink(projectRoot, accessToken) {
|
|
609
|
+
const projectLink = await fetchCloudProjectLink(accessToken);
|
|
610
|
+
writeCloudProjectLink(projectRoot, projectLink);
|
|
611
|
+
}
|
|
575
612
|
function removePath(targetPath) {
|
|
576
613
|
if (!existsSync(targetPath)) return;
|
|
577
614
|
const stat = lstatSync(targetPath);
|
|
@@ -720,7 +757,7 @@ function detectProjectRoot(explicitProjectPath) {
|
|
|
720
757
|
}
|
|
721
758
|
}
|
|
722
759
|
for (const candidate of candidatePaths) {
|
|
723
|
-
if (
|
|
760
|
+
if (hasShowpaneProjectShape(candidate) && existsSync(getProjectMetadataPath(candidate)) && existsSync(getManagedFilesPath(candidate))) {
|
|
724
761
|
return candidate;
|
|
725
762
|
}
|
|
726
763
|
}
|
|
@@ -1201,7 +1238,7 @@ async function createProject(args) {
|
|
|
1201
1238
|
);
|
|
1202
1239
|
}
|
|
1203
1240
|
const authSecret = randomBytes(32).toString("hex");
|
|
1204
|
-
const databaseUrl = "
|
|
1241
|
+
const databaseUrl = `file:${join(projectRoot, "dev.db")}`;
|
|
1205
1242
|
writeFileSync(
|
|
1206
1243
|
join(projectRoot, ".env"),
|
|
1207
1244
|
`DATABASE_URL="${databaseUrl}"
|
|
@@ -1245,6 +1282,12 @@ AUTH_SECRET="${authSecret}"
|
|
|
1245
1282
|
"Check permissions for ~/.showpane and ~/.claude/skills, then try again."
|
|
1246
1283
|
);
|
|
1247
1284
|
}
|
|
1285
|
+
if (config.accessToken) {
|
|
1286
|
+
try {
|
|
1287
|
+
await syncCloudProjectLink(projectRoot, config.accessToken);
|
|
1288
|
+
} catch {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1248
1291
|
stepStartForCreate("Starting app", options);
|
|
1249
1292
|
let serverStart;
|
|
1250
1293
|
try {
|
|
@@ -1452,7 +1495,6 @@ async function login() {
|
|
|
1452
1495
|
config.accessTokenExpiresAt = data.tokenExpiresAt;
|
|
1453
1496
|
config.orgSlug = data.orgSlug;
|
|
1454
1497
|
config.portalUrl = data.portalUrl;
|
|
1455
|
-
config.vercelProjectId = data.vercelProjectId;
|
|
1456
1498
|
const currentWorkspace = findWorkspaceRoot(process.cwd()) ?? (config.app_path ? findWorkspaceRoot(config.app_path) ?? resolve(config.app_path) : null);
|
|
1457
1499
|
if (currentWorkspace) {
|
|
1458
1500
|
updateWorkspaceFromConfig(config, currentWorkspace, {
|
|
@@ -1460,6 +1502,10 @@ async function login() {
|
|
|
1460
1502
|
deployMode: "cloud",
|
|
1461
1503
|
orgSlug: data.orgSlug
|
|
1462
1504
|
});
|
|
1505
|
+
try {
|
|
1506
|
+
await syncCloudProjectLink(currentWorkspace, data.accessToken);
|
|
1507
|
+
} catch {
|
|
1508
|
+
}
|
|
1463
1509
|
} else {
|
|
1464
1510
|
config.deploy_mode = "cloud";
|
|
1465
1511
|
}
|
package/package.json
CHANGED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Multi-stage: build Next.js, copy standalone output, add Caddy
|
|
2
|
-
FROM node:20-alpine AS builder
|
|
3
|
-
WORKDIR /app
|
|
4
|
-
COPY package.json package-lock.json* ./
|
|
5
|
-
COPY prisma ./prisma
|
|
6
|
-
RUN npm ci
|
|
7
|
-
COPY . .
|
|
8
|
-
RUN npx prisma generate && npm run build
|
|
9
|
-
|
|
10
|
-
FROM node:20-alpine AS runner
|
|
11
|
-
WORKDIR /app
|
|
12
|
-
|
|
13
|
-
ENV NODE_ENV=production
|
|
14
|
-
ENV NEXT_TELEMETRY_DISABLED=1
|
|
15
|
-
|
|
16
|
-
# Copy Prisma engine, CLI, and schema for migrations
|
|
17
|
-
COPY --from=builder /app/node_modules/.prisma /app/node_modules/.prisma
|
|
18
|
-
COPY --from=builder /app/node_modules/@prisma /app/node_modules/@prisma
|
|
19
|
-
COPY --from=builder /app/node_modules/prisma /app/node_modules/prisma
|
|
20
|
-
COPY --from=builder /app/node_modules/.bin/prisma /app/node_modules/.bin/prisma
|
|
21
|
-
COPY --from=builder /app/prisma /app/prisma
|
|
22
|
-
|
|
23
|
-
# Copy standalone Next.js output
|
|
24
|
-
COPY --from=builder /app/.next/standalone ./
|
|
25
|
-
COPY --from=builder /app/.next/static ./.next/static
|
|
26
|
-
COPY --from=builder /app/public ./public
|
|
27
|
-
|
|
28
|
-
EXPOSE 3000
|
|
29
|
-
|
|
30
|
-
CMD ["sh", "-c", "node node_modules/prisma/build/index.js migrate deploy && node server.js"]
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
portal:
|
|
3
|
-
build:
|
|
4
|
-
context: .
|
|
5
|
-
dockerfile: docker/Dockerfile
|
|
6
|
-
ports:
|
|
7
|
-
- "3000:3000"
|
|
8
|
-
environment:
|
|
9
|
-
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD:-change-me-in-.env}@db:5432/${POSTGRES_DB:-portal}}
|
|
10
|
-
AUTH_SECRET: ${AUTH_SECRET}
|
|
11
|
-
STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
|
|
12
|
-
volumes:
|
|
13
|
-
- uploads:/app/uploads
|
|
14
|
-
depends_on:
|
|
15
|
-
db:
|
|
16
|
-
condition: service_healthy
|
|
17
|
-
restart: unless-stopped
|
|
18
|
-
|
|
19
|
-
caddy:
|
|
20
|
-
image: caddy:2-alpine
|
|
21
|
-
ports:
|
|
22
|
-
- "80:80"
|
|
23
|
-
- "443:443"
|
|
24
|
-
volumes:
|
|
25
|
-
- ./docker/Caddyfile:/etc/caddy/Caddyfile
|
|
26
|
-
- caddy_data:/data
|
|
27
|
-
- caddy_config:/config
|
|
28
|
-
environment:
|
|
29
|
-
DOMAIN: ${DOMAIN:-localhost}
|
|
30
|
-
depends_on:
|
|
31
|
-
- portal
|
|
32
|
-
restart: unless-stopped
|
|
33
|
-
|
|
34
|
-
db:
|
|
35
|
-
image: postgres:16-alpine
|
|
36
|
-
environment:
|
|
37
|
-
POSTGRES_USER: ${POSTGRES_USER:-portal}
|
|
38
|
-
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me-in-.env}
|
|
39
|
-
POSTGRES_DB: ${POSTGRES_DB:-portal}
|
|
40
|
-
volumes:
|
|
41
|
-
- pgdata:/var/lib/postgresql/data
|
|
42
|
-
healthcheck:
|
|
43
|
-
test: ["CMD-SHELL", "pg_isready -U portal"]
|
|
44
|
-
interval: 5s
|
|
45
|
-
timeout: 5s
|
|
46
|
-
retries: 5
|
|
47
|
-
restart: unless-stopped
|
|
48
|
-
|
|
49
|
-
volumes:
|
|
50
|
-
pgdata:
|
|
51
|
-
uploads:
|
|
52
|
-
caddy_data:
|
|
53
|
-
caddy_config:
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
-- CreateTable
|
|
2
|
-
CREATE TABLE "Organization" (
|
|
3
|
-
"id" TEXT NOT NULL,
|
|
4
|
-
"name" TEXT NOT NULL,
|
|
5
|
-
"slug" TEXT NOT NULL,
|
|
6
|
-
"logoUrl" TEXT,
|
|
7
|
-
"primaryColor" TEXT NOT NULL DEFAULT '#111827',
|
|
8
|
-
"portalLabel" TEXT NOT NULL DEFAULT 'Client Portal',
|
|
9
|
-
"websiteUrl" TEXT,
|
|
10
|
-
"contactName" TEXT NOT NULL,
|
|
11
|
-
"contactTitle" TEXT NOT NULL,
|
|
12
|
-
"contactEmail" TEXT NOT NULL,
|
|
13
|
-
"contactPhone" TEXT,
|
|
14
|
-
"contactAvatar" TEXT,
|
|
15
|
-
"supportEmail" TEXT NOT NULL,
|
|
16
|
-
"customDomain" TEXT,
|
|
17
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
19
|
-
|
|
20
|
-
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
-- CreateTable
|
|
24
|
-
CREATE TABLE "Member" (
|
|
25
|
-
"id" TEXT NOT NULL,
|
|
26
|
-
"organizationId" TEXT NOT NULL,
|
|
27
|
-
"userId" TEXT NOT NULL,
|
|
28
|
-
"role" TEXT NOT NULL DEFAULT 'owner',
|
|
29
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
-
|
|
31
|
-
CONSTRAINT "Member_pkey" PRIMARY KEY ("id")
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
-- CreateTable
|
|
35
|
-
CREATE TABLE "User" (
|
|
36
|
-
"id" TEXT NOT NULL,
|
|
37
|
-
"email" TEXT NOT NULL,
|
|
38
|
-
"name" TEXT,
|
|
39
|
-
"passwordHash" TEXT,
|
|
40
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
41
|
-
|
|
42
|
-
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
-- CreateTable
|
|
46
|
-
CREATE TABLE "ClientPortal" (
|
|
47
|
-
"id" TEXT NOT NULL,
|
|
48
|
-
"organizationId" TEXT NOT NULL,
|
|
49
|
-
"slug" TEXT NOT NULL,
|
|
50
|
-
"companyName" TEXT NOT NULL,
|
|
51
|
-
"logoUrl" TEXT,
|
|
52
|
-
"username" TEXT NOT NULL,
|
|
53
|
-
"passwordHash" TEXT NOT NULL,
|
|
54
|
-
"credentialVersion" TEXT NOT NULL,
|
|
55
|
-
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
|
56
|
-
"lastUpdated" TEXT,
|
|
57
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
58
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
59
|
-
|
|
60
|
-
CONSTRAINT "ClientPortal_pkey" PRIMARY KEY ("id")
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
-- CreateTable
|
|
64
|
-
CREATE TABLE "PortalEvent" (
|
|
65
|
-
"id" TEXT NOT NULL,
|
|
66
|
-
"portalId" TEXT NOT NULL,
|
|
67
|
-
"event" TEXT NOT NULL,
|
|
68
|
-
"detail" TEXT,
|
|
69
|
-
"ipAddress" TEXT,
|
|
70
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
71
|
-
|
|
72
|
-
CONSTRAINT "PortalEvent_pkey" PRIMARY KEY ("id")
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
-- CreateTable
|
|
76
|
-
CREATE TABLE "PortalFile" (
|
|
77
|
-
"id" TEXT NOT NULL,
|
|
78
|
-
"portalId" TEXT NOT NULL,
|
|
79
|
-
"filename" TEXT NOT NULL,
|
|
80
|
-
"mimeType" TEXT NOT NULL,
|
|
81
|
-
"storagePath" TEXT NOT NULL,
|
|
82
|
-
"size" INTEGER NOT NULL,
|
|
83
|
-
"uploadedBy" TEXT NOT NULL DEFAULT 'operator',
|
|
84
|
-
"uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
85
|
-
|
|
86
|
-
CONSTRAINT "PortalFile_pkey" PRIMARY KEY ("id")
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
-- CreateTable
|
|
90
|
-
CREATE TABLE "Session" (
|
|
91
|
-
"id" TEXT NOT NULL,
|
|
92
|
-
"token" TEXT NOT NULL,
|
|
93
|
-
"userId" TEXT NOT NULL,
|
|
94
|
-
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
95
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
-
|
|
97
|
-
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
-- CreateIndex
|
|
101
|
-
CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug");
|
|
102
|
-
|
|
103
|
-
-- CreateIndex
|
|
104
|
-
CREATE UNIQUE INDEX "Organization_customDomain_key" ON "Organization"("customDomain");
|
|
105
|
-
|
|
106
|
-
-- CreateIndex
|
|
107
|
-
CREATE UNIQUE INDEX "Member_organizationId_userId_key" ON "Member"("organizationId", "userId");
|
|
108
|
-
|
|
109
|
-
-- CreateIndex
|
|
110
|
-
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
111
|
-
|
|
112
|
-
-- CreateIndex
|
|
113
|
-
CREATE UNIQUE INDEX "ClientPortal_organizationId_slug_key" ON "ClientPortal"("organizationId", "slug");
|
|
114
|
-
|
|
115
|
-
-- CreateIndex
|
|
116
|
-
CREATE UNIQUE INDEX "ClientPortal_organizationId_username_key" ON "ClientPortal"("organizationId", "username");
|
|
117
|
-
|
|
118
|
-
-- CreateIndex
|
|
119
|
-
CREATE INDEX "PortalEvent_portalId_createdAt_idx" ON "PortalEvent"("portalId", "createdAt");
|
|
120
|
-
|
|
121
|
-
-- CreateIndex
|
|
122
|
-
CREATE UNIQUE INDEX "PortalFile_storagePath_key" ON "PortalFile"("storagePath");
|
|
123
|
-
|
|
124
|
-
-- CreateIndex
|
|
125
|
-
CREATE INDEX "PortalFile_portalId_idx" ON "PortalFile"("portalId");
|
|
126
|
-
|
|
127
|
-
-- CreateIndex
|
|
128
|
-
CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
|
|
129
|
-
|
|
130
|
-
-- AddForeignKey
|
|
131
|
-
ALTER TABLE "Member" ADD CONSTRAINT "Member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
132
|
-
|
|
133
|
-
-- AddForeignKey
|
|
134
|
-
ALTER TABLE "Member" ADD CONSTRAINT "Member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
135
|
-
|
|
136
|
-
-- AddForeignKey
|
|
137
|
-
ALTER TABLE "ClientPortal" ADD CONSTRAINT "ClientPortal_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
138
|
-
|
|
139
|
-
-- AddForeignKey
|
|
140
|
-
ALTER TABLE "PortalEvent" ADD CONSTRAINT "PortalEvent_portalId_fkey" FOREIGN KEY ("portalId") REFERENCES "ClientPortal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
141
|
-
|
|
142
|
-
-- AddForeignKey
|
|
143
|
-
ALTER TABLE "PortalFile" ADD CONSTRAINT "PortalFile_portalId_fkey" FOREIGN KEY ("portalId") REFERENCES "ClientPortal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
generator client {
|
|
2
|
-
provider = "prisma-client"
|
|
3
|
-
output = "../src/generated/prisma"
|
|
4
|
-
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
datasource db {
|
|
8
|
-
provider = "postgresql"
|
|
9
|
-
url = env("DATABASE_URL")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
model Organization {
|
|
13
|
-
id String @id @default(cuid())
|
|
14
|
-
name String
|
|
15
|
-
slug String @unique
|
|
16
|
-
|
|
17
|
-
// Branding
|
|
18
|
-
logoUrl String?
|
|
19
|
-
primaryColor String @default("#111827")
|
|
20
|
-
portalLabel String @default("Client Portal")
|
|
21
|
-
websiteUrl String?
|
|
22
|
-
|
|
23
|
-
// Contact (displayed in portal footer + login)
|
|
24
|
-
contactName String
|
|
25
|
-
contactTitle String
|
|
26
|
-
contactEmail String
|
|
27
|
-
contactPhone String?
|
|
28
|
-
contactAvatar String?
|
|
29
|
-
supportEmail String
|
|
30
|
-
|
|
31
|
-
// Domain
|
|
32
|
-
customDomain String? @unique
|
|
33
|
-
|
|
34
|
-
createdAt DateTime @default(now())
|
|
35
|
-
updatedAt DateTime @updatedAt
|
|
36
|
-
|
|
37
|
-
members Member[]
|
|
38
|
-
portals ClientPortal[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
model Member {
|
|
42
|
-
id String @id @default(cuid())
|
|
43
|
-
organizationId String
|
|
44
|
-
userId String
|
|
45
|
-
role String @default("owner")
|
|
46
|
-
createdAt DateTime @default(now())
|
|
47
|
-
|
|
48
|
-
organization Organization @relation(fields: [organizationId], references: [id])
|
|
49
|
-
user User @relation(fields: [userId], references: [id])
|
|
50
|
-
|
|
51
|
-
@@unique([organizationId, userId])
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
model User {
|
|
55
|
-
id String @id @default(cuid())
|
|
56
|
-
email String @unique
|
|
57
|
-
name String?
|
|
58
|
-
passwordHash String?
|
|
59
|
-
createdAt DateTime @default(now())
|
|
60
|
-
|
|
61
|
-
members Member[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
model ClientPortal {
|
|
65
|
-
id String @id @default(cuid())
|
|
66
|
-
organizationId String
|
|
67
|
-
slug String
|
|
68
|
-
companyName String
|
|
69
|
-
logoUrl String?
|
|
70
|
-
|
|
71
|
-
// Auth
|
|
72
|
-
username String
|
|
73
|
-
passwordHash String
|
|
74
|
-
credentialVersion String @default(cuid())
|
|
75
|
-
isActive Boolean @default(true)
|
|
76
|
-
|
|
77
|
-
// Meta
|
|
78
|
-
lastUpdated String?
|
|
79
|
-
createdAt DateTime @default(now())
|
|
80
|
-
updatedAt DateTime @updatedAt
|
|
81
|
-
|
|
82
|
-
organization Organization @relation(fields: [organizationId], references: [id])
|
|
83
|
-
events PortalEvent[]
|
|
84
|
-
files PortalFile[]
|
|
85
|
-
|
|
86
|
-
@@unique([organizationId, slug])
|
|
87
|
-
@@unique([organizationId, username])
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
model PortalEvent {
|
|
91
|
-
id String @id @default(cuid())
|
|
92
|
-
portalId String
|
|
93
|
-
event String
|
|
94
|
-
detail String?
|
|
95
|
-
visitorId String?
|
|
96
|
-
metadata String?
|
|
97
|
-
ipAddress String?
|
|
98
|
-
createdAt DateTime @default(now())
|
|
99
|
-
|
|
100
|
-
portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
|
|
101
|
-
|
|
102
|
-
@@index([portalId, createdAt])
|
|
103
|
-
@@index([visitorId])
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
model PortalFile {
|
|
107
|
-
id String @id @default(cuid())
|
|
108
|
-
portalId String
|
|
109
|
-
filename String
|
|
110
|
-
mimeType String
|
|
111
|
-
storagePath String @unique
|
|
112
|
-
checksum String?
|
|
113
|
-
size Int
|
|
114
|
-
uploadedBy String @default("operator")
|
|
115
|
-
uploadedAt DateTime @default(now())
|
|
116
|
-
|
|
117
|
-
portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
|
|
118
|
-
|
|
119
|
-
@@index([portalId])
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
model Session {
|
|
123
|
-
id String @id @default(cuid())
|
|
124
|
-
token String @unique
|
|
125
|
-
userId String
|
|
126
|
-
expiresAt DateTime
|
|
127
|
-
createdAt DateTime @default(now())
|
|
128
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# Showpane PostgreSQL backup script
|
|
5
|
-
# Usage: ./scripts/backup.sh [backup_dir]
|
|
6
|
-
|
|
7
|
-
BACKUP_DIR="${1:-./backups}"
|
|
8
|
-
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
9
|
-
FILENAME="showpane-backup-${TIMESTAMP}.sql.gz"
|
|
10
|
-
|
|
11
|
-
mkdir -p "$BACKUP_DIR"
|
|
12
|
-
|
|
13
|
-
echo "Backing up Showpane database..."
|
|
14
|
-
docker compose exec -T db pg_dump --clean -U portal portal | gzip > "${BACKUP_DIR}/${FILENAME}"
|
|
15
|
-
|
|
16
|
-
echo "Backup saved to ${BACKUP_DIR}/${FILENAME}"
|
|
17
|
-
echo ""
|
|
18
|
-
echo "To restore:"
|
|
19
|
-
echo " gunzip -c ${BACKUP_DIR}/${FILENAME} | docker compose exec -T db psql -U portal portal"
|