feat: add admin seed script with default admin account#5
feat: add admin seed script with default admin account#5haphantran wants to merge 2 commits intomainfrom
Conversation
- Create prisma/seed.ts to bootstrap default admin account - If user already exists, promotes to ADMIN role - If user doesn't exist, creates new admin account - Reads ADMIN_EMAIL and ADMIN_PASSWORD from env vars - Deploy workflow now runs migrations + seed after build - Pass ADMIN_EMAIL and ADMIN_PASSWORD through docker-compose.prod.yml
There was a problem hiding this comment.
Pull request overview
Adds a Prisma seed script to ensure a default admin account exists (or is promoted) during production deploys, and wires the deploy workflow to run migrations + seeding.
Changes:
- Introduces
backend/prisma/seed.tsand registers it as Prisma’s seed command. - Updates production deploy workflow to run
prisma migrate deployandprisma db seedafter bringing containers up. - Adds
ADMIN_EMAIL/ADMIN_PASSWORDenvironment variables to production compose + example env file.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docker-compose.prod.yml | Injects ADMIN_* env vars into backend container (currently with a real-email default). |
| backend/prisma/seed.ts | Implements admin bootstrap: promote existing user or create a new ADMIN user. |
| backend/package.json | Registers Prisma seed command (prisma.seed) for prisma db seed. |
| backend/.env.production.example | Documents ADMIN_* variables (currently with a real email). |
| .github/workflows/deploy.yml | Runs migrations + seed in the deployed backend container. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
docker-compose.prod.yml
Outdated
| LOG_LEVEL: info | ||
| JWT_SECRET: ${JWT_SECRET} | ||
| JWT_EXPIRES_IN: 7d | ||
| ADMIN_EMAIL: ${ADMIN_EMAIL:-haphantran@gmail.com} |
There was a problem hiding this comment.
Avoid hardcoding a real email address as the default ADMIN_EMAIL in production. With open registration and no email ownership verification, an attacker can pre-register that email and later be promoted to ADMIN when ops sets ADMIN_PASSWORD. Recommend removing the default entirely (require ADMIN_EMAIL explicitly), and consider failing the container start/seed when ADMIN_EMAIL is unset.
| ADMIN_EMAIL: ${ADMIN_EMAIL:-haphantran@gmail.com} | |
| ADMIN_EMAIL: ${ADMIN_EMAIL:?ADMIN_EMAIL must be set} |
backend/prisma/seed.ts
Outdated
| const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'haphantran@gmail.com'; | ||
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; | ||
|
|
||
| async function main() { |
There was a problem hiding this comment.
ADMIN_EMAIL defaults to a real address here. For production safety, avoid a real default; require ADMIN_EMAIL to be explicitly set (and consider validating it) so you can’t accidentally create/promote an unintended account.
| const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'haphantran@gmail.com'; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; | |
| async function main() { | |
| const ADMIN_EMAIL = process.env.ADMIN_EMAIL; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; | |
| async function main() { | |
| if (!ADMIN_EMAIL) { | |
| console.log('ADMIN_EMAIL not set, skipping admin seed.'); | |
| return; | |
| } |
| if (existing) { | ||
| if (existing.role !== 'ADMIN') { | ||
| await prisma.user.update({ | ||
| where: { id: existing.id }, | ||
| data: { role: 'ADMIN' }, | ||
| }); | ||
| console.log(`Promoted ${ADMIN_EMAIL} to ADMIN.`); | ||
| } else { | ||
| console.log(`${ADMIN_EMAIL} is already ADMIN.`); | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
When an existing user is found, the seed only updates role and keeps the existing password. Because registration doesn’t verify email ownership, a malicious user could register with the ADMIN_EMAIL first and then retain their password after being promoted. Consider also resetting the user’s password hash to ADMIN_PASSWORD when promoting (or require verified-email accounts before allowing promotion).
| await prisma.user.create({ | ||
| data: { | ||
| email: ADMIN_EMAIL.toLowerCase(), | ||
| password: hashedPassword, | ||
| role: 'ADMIN', | ||
| }, | ||
| }); |
There was a problem hiding this comment.
New admin users created via the seed don’t get the same bootstrap data as normal registration (e.g., initializeCoreEcore). On a fresh deploy this can leave the seeded admin account missing required/expected per-user records. Consider reusing the same initialization logic used in AuthService.register for newly created seeded users.
| const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 12); | ||
|
|
There was a problem hiding this comment.
Password hashing cost differs from the rest of the codebase (seed uses 12 rounds, AuthService/AdminService use SALT_ROUNDS=10). Consider reusing the shared constant/config for salt rounds so hashes are consistent and tuning is centralized.
| import { PrismaClient } from '@prisma/client'; | ||
| import bcrypt from 'bcryptjs'; | ||
|
|
||
| const prisma = new PrismaClient(); |
There was a problem hiding this comment.
This seed script lives under prisma/, but backend tsconfig only includes src//* and shared//; CI typecheck won’t catch type errors in this file. Consider adding prisma/**/.ts to the backend tsconfig include (or adding a dedicated typecheck step for the seed) so deploy-time scripts are typechecked.
| JWT_EXPIRES_IN=7d | ||
|
|
||
| # Default admin account (created on first seed run) | ||
| ADMIN_EMAIL=haphantran@gmail.com |
There was a problem hiding this comment.
The production env example includes a specific personal email address. Prefer using a placeholder value (e.g., admin@example.com) so documentation/examples don’t embed personal data and don’t encourage copying a potentially unsafe default into production.
| ADMIN_EMAIL=haphantran@gmail.com | |
| ADMIN_EMAIL=admin@example.com |
.github/workflows/deploy.yml
Outdated
| docker compose -f docker-compose.prod.yml exec -T backend npx prisma migrate deploy | ||
| docker compose -f docker-compose.prod.yml exec -T backend npx prisma db seed | ||
| docker image prune -f |
There was a problem hiding this comment.
Deploy workflow runs prisma migrate deploy inside the backend container after docker compose up -d --build, but the backend image already runs npx prisma migrate deploy on container start (see backend/Dockerfile CMD). This redundancy can also introduce a race where migrations run concurrently. Recommend running migrations in exactly one place (either keep it in container startup and drop this step, or remove it from CMD and keep it here) and ensure seed runs after migrations deterministically.
| docker compose -f docker-compose.prod.yml exec -T backend npx prisma migrate deploy | |
| docker compose -f docker-compose.prod.yml exec -T backend npx prisma db seed | |
| docker image prune -f | |
| docker compose -f docker-compose.prod.yml exec -T backend npx prisma db seed | |
| docker image prune -f |
- Deploy workflow passes ADMIN_EMAIL and ADMIN_PASSWORD from GitHub secrets via SSH - Remove hardcoded default email from seed script and docker-compose - Seed skips if either ADMIN_EMAIL or ADMIN_PASSWORD is not set
Summary
backend/prisma/seed.tsto bootstrap a default admin account on deployprisma migrate deploy+prisma db seedafter buildADMIN_EMAILandADMIN_PASSWORDenv vars todocker-compose.prod.ymlSetup
Add to
.envon VPS:Test plan