showpane 0.4.0 → 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 +22 -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 +875 -159
- package/package.json +4 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "showpane-portal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"prisma:generate": "node scripts/prisma-generate.mjs",
|
|
8
|
+
"prisma:db-push": "node scripts/prisma-db-push.mjs",
|
|
9
|
+
"build": "npm run prisma:generate && next build",
|
|
10
|
+
"start": "next start",
|
|
11
|
+
"postinstall": "npm run prisma:generate",
|
|
12
|
+
"db:migrate": "prisma migrate deploy",
|
|
13
|
+
"db:push": "npm run prisma:db-push",
|
|
14
|
+
"db:seed": "npx tsx prisma/seed.ts",
|
|
15
|
+
"test": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@aws-sdk/client-s3": "^3.1027.0",
|
|
19
|
+
"@prisma/client": "6.19.2",
|
|
20
|
+
"bcryptjs": "^2.4.3",
|
|
21
|
+
"clsx": "^2.1.1",
|
|
22
|
+
"lucide-react": "^0.468.0",
|
|
23
|
+
"next": "^15.3.1",
|
|
24
|
+
"react": "^19.1.0",
|
|
25
|
+
"react-dom": "^19.1.0",
|
|
26
|
+
"tailwind-merge": "^3.0.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"adm-zip": "^0.5.17",
|
|
30
|
+
"@types/bcryptjs": "^2.4.6",
|
|
31
|
+
"@types/node": "^22.15.2",
|
|
32
|
+
"@types/react": "^19.1.2",
|
|
33
|
+
"@types/react-dom": "^19.1.2",
|
|
34
|
+
"autoprefixer": "^10.4.21",
|
|
35
|
+
"postcss": "^8.5.3",
|
|
36
|
+
"prisma": "6.19.2",
|
|
37
|
+
"tailwindcss": "^3.4.17",
|
|
38
|
+
"tsx": "^4.19.4",
|
|
39
|
+
"typescript": "^5.8.3",
|
|
40
|
+
"vitest": "^4.1.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Local development schema — SQLite provider for zero-config local dev.
|
|
2
|
+
// Used by `npx showpane` installer via: prisma db push --schema prisma/schema.local.prisma
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client"
|
|
6
|
+
output = "../src/generated/prisma"
|
|
7
|
+
binaryTargets = ["native"]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
datasource db {
|
|
11
|
+
provider = "sqlite"
|
|
12
|
+
url = env("DATABASE_URL")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
model Organization {
|
|
16
|
+
id String @id @default(cuid())
|
|
17
|
+
name String
|
|
18
|
+
slug String @unique
|
|
19
|
+
|
|
20
|
+
// Branding
|
|
21
|
+
logoUrl String?
|
|
22
|
+
primaryColor String @default("#111827")
|
|
23
|
+
portalLabel String @default("Client Portal")
|
|
24
|
+
websiteUrl String?
|
|
25
|
+
|
|
26
|
+
// Contact (displayed in portal footer + login)
|
|
27
|
+
contactName String
|
|
28
|
+
contactTitle String
|
|
29
|
+
contactEmail String
|
|
30
|
+
contactPhone String?
|
|
31
|
+
contactAvatar String?
|
|
32
|
+
supportEmail String
|
|
33
|
+
|
|
34
|
+
// Domain
|
|
35
|
+
customDomain String? @unique
|
|
36
|
+
|
|
37
|
+
createdAt DateTime @default(now())
|
|
38
|
+
updatedAt DateTime @updatedAt
|
|
39
|
+
|
|
40
|
+
members Member[]
|
|
41
|
+
portals ClientPortal[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
model Member {
|
|
45
|
+
id String @id @default(cuid())
|
|
46
|
+
organizationId String
|
|
47
|
+
userId String
|
|
48
|
+
role String @default("owner")
|
|
49
|
+
createdAt DateTime @default(now())
|
|
50
|
+
|
|
51
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
52
|
+
user User @relation(fields: [userId], references: [id])
|
|
53
|
+
|
|
54
|
+
@@unique([organizationId, userId])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
model User {
|
|
58
|
+
id String @id @default(cuid())
|
|
59
|
+
email String @unique
|
|
60
|
+
name String?
|
|
61
|
+
passwordHash String?
|
|
62
|
+
createdAt DateTime @default(now())
|
|
63
|
+
|
|
64
|
+
members Member[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
model ClientPortal {
|
|
68
|
+
id String @id @default(cuid())
|
|
69
|
+
organizationId String
|
|
70
|
+
slug String
|
|
71
|
+
companyName String
|
|
72
|
+
logoUrl String?
|
|
73
|
+
|
|
74
|
+
// Auth
|
|
75
|
+
username String
|
|
76
|
+
passwordHash String
|
|
77
|
+
credentialVersion String @default(cuid())
|
|
78
|
+
isActive Boolean @default(true)
|
|
79
|
+
|
|
80
|
+
// Meta
|
|
81
|
+
lastUpdated String?
|
|
82
|
+
createdAt DateTime @default(now())
|
|
83
|
+
updatedAt DateTime @updatedAt
|
|
84
|
+
|
|
85
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
86
|
+
events PortalEvent[]
|
|
87
|
+
files PortalFile[]
|
|
88
|
+
|
|
89
|
+
@@unique([organizationId, slug])
|
|
90
|
+
@@unique([organizationId, username])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
model PortalEvent {
|
|
94
|
+
id String @id @default(cuid())
|
|
95
|
+
portalId String
|
|
96
|
+
event String
|
|
97
|
+
detail String?
|
|
98
|
+
visitorId String?
|
|
99
|
+
metadata String?
|
|
100
|
+
ipAddress String?
|
|
101
|
+
createdAt DateTime @default(now())
|
|
102
|
+
|
|
103
|
+
portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
|
|
104
|
+
|
|
105
|
+
@@index([portalId, createdAt])
|
|
106
|
+
@@index([visitorId])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
model PortalFile {
|
|
110
|
+
id String @id @default(cuid())
|
|
111
|
+
portalId String
|
|
112
|
+
filename String
|
|
113
|
+
mimeType String
|
|
114
|
+
storagePath String @unique
|
|
115
|
+
checksum String?
|
|
116
|
+
size Int
|
|
117
|
+
uploadedBy String @default("operator")
|
|
118
|
+
uploadedAt DateTime @default(now())
|
|
119
|
+
|
|
120
|
+
portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
|
|
121
|
+
|
|
122
|
+
@@index([portalId])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
model Session {
|
|
126
|
+
id String @id @default(cuid())
|
|
127
|
+
token String @unique
|
|
128
|
+
userId String
|
|
129
|
+
expiresAt DateTime
|
|
130
|
+
createdAt DateTime @default(now())
|
|
131
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
|
|
4
|
+
const prisma = new PrismaClient();
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const org = await prisma.organization.upsert({
|
|
8
|
+
where: { slug: "demo" },
|
|
9
|
+
update: {},
|
|
10
|
+
create: {
|
|
11
|
+
name: "Demo Company",
|
|
12
|
+
slug: "demo",
|
|
13
|
+
contactName: "Jane Smith",
|
|
14
|
+
contactTitle: "Account Manager",
|
|
15
|
+
contactEmail: "jane@example.com",
|
|
16
|
+
supportEmail: "support@example.com",
|
|
17
|
+
websiteUrl: "https://example.com",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const passwordHash = await bcrypt.hash("demo-only-password", 10);
|
|
22
|
+
|
|
23
|
+
await prisma.clientPortal.upsert({
|
|
24
|
+
where: {
|
|
25
|
+
organizationId_slug: {
|
|
26
|
+
organizationId: org.id,
|
|
27
|
+
slug: "example",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
update: {},
|
|
31
|
+
create: {
|
|
32
|
+
organizationId: org.id,
|
|
33
|
+
slug: "example",
|
|
34
|
+
companyName: "Acme Health",
|
|
35
|
+
username: "example",
|
|
36
|
+
passwordHash,
|
|
37
|
+
lastUpdated: "2 April 2026",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log("Seed complete: org 'demo', portal 'example' (username: example, password: demo-only-password)");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main()
|
|
45
|
+
.catch((e) => {
|
|
46
|
+
console.error(e);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
})
|
|
49
|
+
.finally(() => prisma.$disconnect());
|
|
@@ -0,0 +1,19 @@
|
|
|
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"
|