This commit is contained in:
parent
abc7f77622
commit
8c6390d6ac
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) |
|
||||
| `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 |
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { PrismaClient, Role } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
const { PrismaClient, Role } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -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
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
// 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<PropertyKey, unknown>)[prop];
|
||||
return typeof value === "function" ? value.bind(client) : value;
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user