diff --git a/Dockerfile b/Dockerfile index 925b796..dc6b5f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,11 @@ COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma +# The `prisma` CLI is not part of the standalone trace, and `npx prisma` +# fails non-interactively in the runner. Install it (pinned to the project's +# Prisma version) so `scripts/start.sh` can run `prisma migrate deploy`. +RUN npm install --no-save --no-audit --no-fund prisma@5.22.0 + COPY scripts/start.sh /app/start.sh RUN chmod +x /app/start.sh diff --git a/docs/admin.md b/docs/admin.md index a7a8a98..dce4bbe 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -36,10 +36,10 @@ Username and bcrypt-hashed password are read from env: Because `Code.createdById` is a non-nullable FK to `AdminUser`, the env super-admin also needs a row in `AdminUser`. Provisioning is -handled by `prisma/seed.ts`, which upserts the SUPER_ADMIN row from -the same env values. Run `npm run db:seed` after starting the DB -(or the container's `start.sh` will keep migrations up to date on -boot; seed is run manually or via CI as needed). +handled by `prisma/seed.cjs`, which upserts the SUPER_ADMIN row from +the same env values. Run `npm run db:seed` after starting the DB; the +container's `scripts/start.sh` also runs it automatically on every +boot (after `prisma migrate deploy`). ## Codes diff --git a/docs/adminImplem.md b/docs/adminImplem.md index faef72d..2248bbb 100644 --- a/docs/adminImplem.md +++ b/docs/adminImplem.md @@ -13,7 +13,7 @@ > bcrypt) — see `docs/admin.md`. > - **Super-admin row in `AdminUser`**: planned as optional seed; > required by the `Code.createdById` FK and provisioned by -> `prisma/seed.ts`. +> `prisma/seed.cjs` (run by `scripts/start.sh` on boot). > - **Rate limiting, CSRF/Origin checks, password complexity, atomic > code claim** (updateMany guard): all added in Phases 1–6 and not > in the original plan. diff --git a/docs/coolify.md b/docs/coolify.md index cb400f0..c11afa5 100644 --- a/docs/coolify.md +++ b/docs/coolify.md @@ -109,6 +109,15 @@ S3_BUCKET_NAME=monuments-images NEXT_PUBLIC_APP_URL=https://testbed.mk NEXT_PUBLIC_APP_DOMAIN=testbed.mk +# Admin session signing secret (64+ random hex chars; openssl rand -hex 32) +ADMIN_SESSION_SECRET=your-random-64-char-secret + +# Super-admin — username + BCRYPT HASH (not plaintext). Generate with: +# node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))" +# Coolify UI env vars are passed to the container literally — no `$` escaping needed. +SUPER_ADMIN_USERNAME=super +SUPER_ADMIN_PASSWORD_HASH=$2b$12$REPLACE_WITH_BCRYPT_HASH + # Node NODE_ENV=production ``` @@ -116,6 +125,11 @@ NODE_ENV=production **Important:** - `DATABASE_URL` must point to the Coolify **internal** hostname (`spomeniqr-db`), not `localhost`. - Use your **production** Clerk keys (`pk_live_` / `sk_live_`), not the test ones. +- Use a **different, strong** super-admin password than your local development one. +- The `super` admin row is provisioned automatically on container start by + `scripts/start.sh` (`node prisma/seed.cjs` after migrations). If you instead + use a custom Nixpacks start command (Option A below), run the seed manually + after the first deploy: `npx prisma db seed`. ## Step 5: Configure Domain & Subdomain Routing @@ -315,6 +329,9 @@ npx prisma db push | `S3_BUCKET_NAME` | Yes | S3 bucket name | | `NEXT_PUBLIC_APP_URL` | Yes | `https://testbed.mk` | | `NEXT_PUBLIC_APP_DOMAIN` | Yes | `testbed.mk` | +| `ADMIN_SESSION_SECRET` | Yes | Secret signing admin session cookies (openssl rand -hex 32) | +| `SUPER_ADMIN_USERNAME` | No | Super-admin username (default `super`) | +| `SUPER_ADMIN_PASSWORD_HASH` | No | Super-admin bcrypt hash; if unset, super login is unavailable | | `NODE_ENV` | Yes | `production` | ## Useful Coolify Commands diff --git a/docs/deploy.md b/docs/deploy.md index ef62802..35b7b36 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -104,9 +104,24 @@ S3_BUCKET_NAME=monuments-images # App NEXT_PUBLIC_APP_URL=https://testbed.mk NEXT_PUBLIC_APP_DOMAIN=testbed.mk + +# Admin session signing secret (64+ random hex chars; openssl rand -hex 32) +ADMIN_SESSION_SECRET=your-random-64-char-secret + +# Super-admin — username + BCRYPT HASH (not plaintext) of the super-admin password. +# Generate the hash with: +# node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))" +# If you run the app via `docker compose` with these vars in a `.env` env_file, +# you MUST escape every `$` as `$$` (e.g. `$$2b$$12$$...`). Newer Compose +# versions interpolate env_file values and will otherwise strip the `$2b$12` +# prefix, silently breaking super-admin login. If the vars are provided via a +# platform UI (Coolify/Vercel) they are passed literally and need no escaping. +SUPER_ADMIN_USERNAME=super +SUPER_ADMIN_PASSWORD_HASH=$2b$12$REPLACE_WITH_BCRYPT_HASH ``` -**Important**: Use a strong, unique password for `POSTGRES_PASSWORD`. +**Important**: Use a strong, unique password for `POSTGRES_PASSWORD`. Do **not** +reuse the development super-admin password (`Irina@7654321`) in production. ### Create the Prisma Migration @@ -135,6 +150,14 @@ npx prisma migrate dev --name init Then commit the generated migration files. The `scripts/start.sh` entrypoint will run `npx prisma migrate deploy` automatically on every container start. +### Super-admin provisioning + +The `super` admin is provisioned from the environment on **every container +start**: `scripts/start.sh` runs `node prisma/seed.cjs` after migrations. It +upserts the `SUPER_ADMIN_USERNAME` row (role `SUPER_ADMIN`) with the bcrypt hash +from `SUPER_ADMIN_PASSWORD_HASH`. If the hash is unset the seed is skipped +(logged). No manual seeding is required on first deploy. + ## 4. Configure Contabo S3 ### Create the Bucket @@ -461,4 +484,7 @@ docker compose exec app printenv DATABASE_URL | `S3_SECRET_ACCESS_KEY` | Yes | S3 secret key | | `S3_BUCKET_NAME` | Yes | S3 bucket name (monuments-images) | | `NEXT_PUBLIC_APP_URL` | Yes | Public URL (https://testbed.mk) | -| `NEXT_PUBLIC_APP_DOMAIN` | Yes | Domain only (testbed.mk) | \ No newline at end of file +| `NEXT_PUBLIC_APP_DOMAIN` | Yes | Domain only (testbed.mk) | +| `ADMIN_SESSION_SECRET` | Yes | Secret signing admin session cookies (openssl rand -hex 32) | +| `SUPER_ADMIN_USERNAME` | No | Super-admin username (default `super`) | +| `SUPER_ADMIN_PASSWORD_HASH` | No | Super-admin bcrypt hash; if unset, super login is unavailable | \ No newline at end of file diff --git a/package.json b/package.json index 31bcc24..5429a3d 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "db:push": "prisma db push", "db:studio": "prisma studio", "db:generate": "prisma generate", - "db:seed": "tsx prisma/seed.ts" + "db:seed": "node prisma/seed.cjs" }, "prisma": { - "seed": "tsx prisma/seed.ts" + "seed": "node prisma/seed.cjs" }, "dependencies": { "@aws-sdk/client-s3": "^3.1073.0", diff --git a/prisma/seed.ts b/prisma/seed.cjs similarity index 91% rename from prisma/seed.ts rename to prisma/seed.cjs index fd3c898..7ece68f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.cjs @@ -1,5 +1,4 @@ -import { PrismaClient, Role } from "@prisma/client"; -import bcrypt from "bcryptjs"; +const { PrismaClient, Role } = require("@prisma/client"); const prisma = new PrismaClient(); diff --git a/scripts/start.sh b/scripts/start.sh index e11c214..82ee6b8 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -14,5 +14,8 @@ if ! npx prisma migrate deploy; then exit 1 fi +echo "Provisioning super-admin (skips if SUPER_ADMIN_PASSWORD_HASH unset)..." +node prisma/seed.cjs + echo "Starting Next.js server on 0.0.0.0:3000..." exec node server.js \ No newline at end of file diff --git a/src/app/admin/(panel)/layout.tsx b/src/app/admin/(panel)/layout.tsx index 21748ee..0bd887a 100644 --- a/src/app/admin/(panel)/layout.tsx +++ b/src/app/admin/(panel)/layout.tsx @@ -2,6 +2,10 @@ import { getAdminSession } from "@/lib/admin-session"; import { redirect } from "next/navigation"; import AdminSidebar from "./AdminSidebar"; +// Admin pages must always render against the live session — never at build +// time (static generation would run DB queries during `next build`). +export const dynamic = "force-dynamic"; + export default async function AdminLayout({ children }: { children: React.ReactNode }) { const session = await getAdminSession(); if (!session) { diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 6e35df7..bb97479 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -4,6 +4,22 @@ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); +function getPrisma(): PrismaClient { + if (!globalForPrisma.prisma) { + globalForPrisma.prisma = new PrismaClient(); + } + return globalForPrisma.prisma; +} -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; \ No newline at end of file +// Lazy singleton: `new PrismaClient()` throws during `next build` when +// DATABASE_URL is absent (e.g. in a Docker builder stage where env files +// aren't present). Defer construction until the first real query so merely +// importing this module never fails. Method calls are bound to the real +// instance so `this` is preserved. +export const prisma = new Proxy({} as PrismaClient, { + get(_target, prop) { + const client = getPrisma(); + const value = (client as unknown as Record)[prop]; + return typeof value === "function" ? value.bind(client) : value; + }, +});