From d5e7a9a0b54264060f8a426006f76b254d924aa2 Mon Sep 17 00:00:00 2001 From: dimitar Date: Sun, 2 Aug 2026 14:47:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(schema):=20Phase=206=20=E2=80=94=20VarChar?= =?UTF-8?q?=20bounds,=20updatedAt,=20key=20index,=20prisma=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema hardening pass. After reading the onboarding/edit forms I reconsidered the original 'migrate String? -> DateTime?' suggestion: the bornDate/passedDate placeholders are 'нпр. 1960' and the column deliberately accepts imprecise values like '1960', 'early 1990s', '12 May 1960'. Forcing DateTime? would silently break that feature and require users to enter exact dates they often don't know. The responsible fix is to keep the free-text semantics but bound the column length and document the intent in a schema comment. Future structured-date queries should add a parallel DateTime? column. Schema changes (prisma/schema.prisma): - @db.VarChar bounds added to every string column, sized to match the existing client-side maxLength / app-constant caps: User.subdomain -> 32, User.title -> 100, User.description -> 2000, User.bornDate/passedDate -> 50, User.email -> 255, etc. Image.url/key -> 255, Image.id/userId -> 30. AdminUser.username -> 50, passwordHash -> 100. Code.code -> 12, usedByUserId -> 100 (matches Clerk userId scale). - Image.key: added @@index('Image_key_idx') to support the /api/image lookup we tightened in Phase 1 (currently a scan). - updatedAt added to Image, AdminUser, Code (previously only User). - Inline schema comment on Code.createdById documenting the existing onDelete: Restrict behaviour (Prisma default) — the migration does not change it, just makes the audit-trail intent explicit. Migration: - 20260802000000_phase6_schema_hardening/migration.sql: explicit ALTER TABLE ... SET DATA TYPE VARCHAR(N) statements for every bounded column; ADD COLUMN updatedAt with DEFAULT CURRENT_TIMESTAMP (so existing rows back-fill immediately); CREATE INDEX for the Image.key column; a defensive LEFT() truncation UPDATE for any rows whose bornDate/passedDate exceed 50 chars (the inputs always capped at 50 on the client side, so this is belt-and-braces). Will be applied on next deploy via 'prisma migrate deploy'. Seed: - prisma/seed.ts: upserts the SUPER_ADMIN row from env SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORD_HASH. This resolves the Phase 1 problem where an env-only super-admin had no row in AdminUser, which meant the Code.createdById FK prevented them from ever creating codes. The seed is idempotent and safe to run on every boot (uses upsert). - package.json: 'prisma.seed' wired to 'tsx prisma/seed.ts'. - 'db:seed' npm script also added for manual provisioning. - tsx added as devDependency (executes TypeScript straight from disk without pre-build). --- package-lock.json | 504 ++++++++++++++++++ package.json | 7 +- .../migration.sql | 43 ++ prisma/schema.prisma | 60 ++- prisma/seed.ts | 43 ++ 5 files changed, 631 insertions(+), 26 deletions(-) create mode 100644 prisma/migrations/20260802000000_phase6_schema_hardening/migration.sql create mode 100644 prisma/seed.ts diff --git a/package-lock.json b/package-lock.json index 385f150..1c20a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "eslint-config-next": "^15.5.19", "prisma": "^5.22.0", "tailwindcss": "^4", + "tsx": "^4.23.3", "typescript": "^5", "vitest": "^2.1.9" } @@ -889,6 +890,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -906,6 +924,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -923,6 +958,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -7762,6 +7814,458 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.3.tgz", + "integrity": "sha512-hahlTkAAf5cqMiD8V2b+UgasXreAb2D1tgcYkLegeacgcDH11fY0gqrFJRzlpBDZkFJrVuPvIVMSc7rvJpHLLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, diff --git a/package.json b/package.json index 1ed8e19..31bcc24 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "db:migrate": "prisma migrate dev", "db:push": "prisma db push", "db:studio": "prisma studio", - "db:generate": "prisma generate" + "db:generate": "prisma generate", + "db:seed": "tsx prisma/seed.ts" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1073.0", @@ -40,6 +44,7 @@ "eslint-config-next": "^15.5.19", "prisma": "^5.22.0", "tailwindcss": "^4", + "tsx": "^4.23.3", "typescript": "^5", "vitest": "^2.1.9" } diff --git a/prisma/migrations/20260802000000_phase6_schema_hardening/migration.sql b/prisma/migrations/20260802000000_phase6_schema_hardening/migration.sql new file mode 100644 index 0000000..b4815f6 --- /dev/null +++ b/prisma/migrations/20260802000000_phase6_schema_hardening/migration.sql @@ -0,0 +1,43 @@ +-- Phase 6 schema hardening +-- Adds @db.VarChar() length bounds to all string columns, updatedAt to +-- Image/AdminUser/Code, an index on Image.key, and documents the +-- existing Code.createdById onDelete:RESTRICT behaviour with a schema +-- comment (no DDL change there; it was already RESTRICT by default). + +-- === User: add VarChar bounds === +ALTER TABLE "User" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30); +ALTER TABLE "User" ALTER COLUMN "clerkId" SET DATA TYPE VARCHAR(100); +ALTER TABLE "User" ALTER COLUMN "email" SET DATA TYPE VARCHAR(255); +ALTER TABLE "User" ALTER COLUMN "name" SET DATA TYPE VARCHAR(100); +ALTER TABLE "User" ALTER COLUMN "subdomain" SET DATA TYPE VARCHAR(32); +ALTER TABLE "User" ALTER COLUMN "title" SET DATA TYPE VARCHAR(100); +ALTER TABLE "User" ALTER COLUMN "description" SET DATA TYPE VARCHAR(2000); +ALTER TABLE "User" ALTER COLUMN "bornDate" SET DATA TYPE VARCHAR(50); +ALTER TABLE "User" ALTER COLUMN "passedDate" SET DATA TYPE VARCHAR(50); + +-- === Image: bounds + updatedAt + index on key === +ALTER TABLE "Image" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30); +ALTER TABLE "Image" ALTER COLUMN "url" SET DATA TYPE VARCHAR(255); +ALTER TABLE "Image" ALTER COLUMN "key" SET DATA TYPE VARCHAR(255); +ALTER TABLE "Image" ALTER COLUMN "userId" SET DATA TYPE VARCHAR(30); +ALTER TABLE "Image" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; +CREATE INDEX "Image_key_idx" ON "Image"("key"); + +-- === AdminUser: bounds + updatedAt === +ALTER TABLE "AdminUser" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30); +ALTER TABLE "AdminUser" ALTER COLUMN "username" SET DATA TYPE VARCHAR(50); +ALTER TABLE "AdminUser" ALTER COLUMN "passwordHash" SET DATA TYPE VARCHAR(100); +ALTER TABLE "AdminUser" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- === Code: bounds + updatedAt === +ALTER TABLE "Code" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30); +ALTER TABLE "Code" ALTER COLUMN "code" SET DATA TYPE VARCHAR(12); +ALTER TABLE "Code" ALTER COLUMN "createdById" SET DATA TYPE VARCHAR(30); +ALTER TABLE "Code" ALTER COLUMN "usedByUserId" SET DATA TYPE VARCHAR(100); +ALTER TABLE "Code" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- Backfill any rows whose string columns exceed the new bounds (defensive; +-- production data was capped at the maxLength on the input side already but +-- the persistence layer was unbounded). Truncating is safer than failing. +UPDATE "User" SET "bornDate" = LEFT("bornDate", 50) WHERE "bornDate" IS NOT NULL AND LENGTH("bornDate") > 50; +UPDATE "User" SET "passedDate" = LEFT("passedDate", 50) WHERE "passedDate" IS NOT NULL AND LENGTH("passedDate") > 50; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6504a95..c7e2b90 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,32 +9,37 @@ datasource db { } model User { - id String @id @default(cuid()) - clerkId String @unique - email String? - name String? - subdomain String @unique - templateId Int @default(1) - title String? - description String? - bornDate String? - passedDate String? - published Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) @db.VarChar(30) + clerkId String @unique @db.VarChar(100) + email String? @db.VarChar(255) + name String? @db.VarChar(100) + subdomain String @unique @db.VarChar(32) + templateId Int @default(1) + title String? @db.VarChar(100) + description String? @db.VarChar(2000) + // Free-form text — intentionally accepts imprecise values like "1960" + // or "early 1990s", not a parseable date. If structured date queries + // become needed, add a parallel bornDateParsed DateTime? column. + bornDate String? @db.VarChar(50) + passedDate String? @db.VarChar(50) + published Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt images Image[] } model Image { - id String @id @default(cuid()) - url String - key String + id String @id @default(cuid()) @db.VarChar(30) + url String @db.VarChar(255) + key String @db.VarChar(255) order Int - userId String + userId String @db.VarChar(30) user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([userId]) + @@index([key]) } enum Role { @@ -43,23 +48,28 @@ enum Role { } model AdminUser { - id String @id @default(cuid()) - username String @unique - passwordHash String + id String @id @default(cuid()) @db.VarChar(30) + username String @unique @db.VarChar(50) + passwordHash String @db.VarChar(100) role Role @default(ADMIN) createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt createdCodes Code[] } model Code { - id String @id @default(cuid()) - code String @unique - createdById String + id String @id @default(cuid()) @db.VarChar(30) + code String @unique @db.VarChar(12) + // onDelete: Restrict (Prisma default) — preventing deletion of an + // AdminUser that has issued codes is intentional; we don't want + // orphaned codes with no audit trail of who created them. + createdById String @db.VarChar(30) createdBy AdminUser @relation(fields: [createdById], references: [id]) - usedByUserId String? + usedByUserId String? @db.VarChar(100) usedAt DateTime? createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([code]) @@index([usedByUserId]) -} \ No newline at end of file +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..fd3c898 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,43 @@ +import { PrismaClient, Role } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const prisma = new PrismaClient(); + +async function main() { + const username = process.env.SUPER_ADMIN_USERNAME || "super"; + const hash = process.env.SUPER_ADMIN_PASSWORD_HASH; + + if (!hash) { + console.warn( + "[seed] SUPER_ADMIN_PASSWORD_HASH env not set; skipping super-admin provisioning." + ); + console.warn( + "[seed] Generate one with: node -e \"import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))\"" + ); + return; + } + + await prisma.adminUser.upsert({ + where: { username }, + update: { + passwordHash: hash, + role: Role.SUPER_ADMIN, + }, + create: { + username, + passwordHash: hash, + role: Role.SUPER_ADMIN, + }, + }); + + console.log(`[seed] Super-admin '${username}' provisioned.`); +} + +main() + .catch((e) => { + console.error("[seed] failed:", e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + });