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
|
||||||
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
|
COPY scripts/start.sh /app/start.sh
|
||||||
RUN chmod +x /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
|
Because `Code.createdById` is a non-nullable FK to `AdminUser`, the
|
||||||
env super-admin also needs a row in `AdminUser`. Provisioning is
|
env super-admin also needs a row in `AdminUser`. Provisioning is
|
||||||
handled by `prisma/seed.ts`, which upserts the SUPER_ADMIN row from
|
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 same env values. Run `npm run db:seed` after starting the DB; the
|
||||||
(or the container's `start.sh` will keep migrations up to date on
|
container's `scripts/start.sh` also runs it automatically on every
|
||||||
boot; seed is run manually or via CI as needed).
|
boot (after `prisma migrate deploy`).
|
||||||
|
|
||||||
## Codes
|
## Codes
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
> bcrypt) — see `docs/admin.md`.
|
> bcrypt) — see `docs/admin.md`.
|
||||||
> - **Super-admin row in `AdminUser`**: planned as optional seed;
|
> - **Super-admin row in `AdminUser`**: planned as optional seed;
|
||||||
> required by the `Code.createdById` FK and provisioned by
|
> 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
|
> - **Rate limiting, CSRF/Origin checks, password complexity, atomic
|
||||||
> code claim** (updateMany guard): all added in Phases 1–6 and not
|
> code claim** (updateMany guard): all added in Phases 1–6 and not
|
||||||
> in the original plan.
|
> in the original plan.
|
||||||
|
|||||||
@ -109,6 +109,15 @@ S3_BUCKET_NAME=monuments-images
|
|||||||
NEXT_PUBLIC_APP_URL=https://testbed.mk
|
NEXT_PUBLIC_APP_URL=https://testbed.mk
|
||||||
NEXT_PUBLIC_APP_DOMAIN=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
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
```
|
```
|
||||||
@ -116,6 +125,11 @@ NODE_ENV=production
|
|||||||
**Important:**
|
**Important:**
|
||||||
- `DATABASE_URL` must point to the Coolify **internal** hostname (`spomeniqr-db`), not `localhost`.
|
- `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 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
|
## Step 5: Configure Domain & Subdomain Routing
|
||||||
|
|
||||||
@ -315,6 +329,9 @@ npx prisma db push
|
|||||||
| `S3_BUCKET_NAME` | Yes | S3 bucket name |
|
| `S3_BUCKET_NAME` | Yes | S3 bucket name |
|
||||||
| `NEXT_PUBLIC_APP_URL` | Yes | `https://testbed.mk` |
|
| `NEXT_PUBLIC_APP_URL` | Yes | `https://testbed.mk` |
|
||||||
| `NEXT_PUBLIC_APP_DOMAIN` | Yes | `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` |
|
| `NODE_ENV` | Yes | `production` |
|
||||||
|
|
||||||
## Useful Coolify Commands
|
## Useful Coolify Commands
|
||||||
|
|||||||
@ -104,9 +104,24 @@ S3_BUCKET_NAME=monuments-images
|
|||||||
# App
|
# App
|
||||||
NEXT_PUBLIC_APP_URL=https://testbed.mk
|
NEXT_PUBLIC_APP_URL=https://testbed.mk
|
||||||
NEXT_PUBLIC_APP_DOMAIN=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
|
### 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.
|
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
|
## 4. Configure Contabo S3
|
||||||
|
|
||||||
### Create the Bucket
|
### Create the Bucket
|
||||||
@ -462,3 +485,6 @@ docker compose exec app printenv DATABASE_URL
|
|||||||
| `S3_BUCKET_NAME` | Yes | S3 bucket name (monuments-images) |
|
| `S3_BUCKET_NAME` | Yes | S3 bucket name (monuments-images) |
|
||||||
| `NEXT_PUBLIC_APP_URL` | Yes | Public URL (https://testbed.mk) |
|
| `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:push": "prisma db push",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:seed": "tsx prisma/seed.ts"
|
"db:seed": "node prisma/seed.cjs"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "tsx prisma/seed.ts"
|
"seed": "node prisma/seed.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1073.0",
|
"@aws-sdk/client-s3": "^3.1073.0",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { PrismaClient, Role } from "@prisma/client";
|
const { PrismaClient, Role } = require("@prisma/client");
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@ -14,5 +14,8 @@ if ! npx prisma migrate deploy; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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..."
|
echo "Starting Next.js server on 0.0.0.0:3000..."
|
||||||
exec node server.js
|
exec node server.js
|
||||||
@ -2,6 +2,10 @@ import { getAdminSession } from "@/lib/admin-session";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import AdminSidebar from "./AdminSidebar";
|
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 }) {
|
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
const session = await getAdminSession();
|
const session = await getAdminSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|||||||
@ -4,6 +4,22 @@ const globalForPrisma = globalThis as unknown as {
|
|||||||
prisma: PrismaClient | undefined;
|
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