From 2e718a39602ee0d0bcebd006f8d73f3e2abfd50e Mon Sep 17 00:00:00 2001 From: Ha Phan Tran Date: Mon, 30 Mar 2026 15:29:37 -0400 Subject: [PATCH 1/2] feat: add admin seed script and run on deploy - 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 --- .github/workflows/deploy.yml | 2 ++ backend/.env.production.example | 4 +++ backend/package.json | 3 ++ backend/prisma/seed.ts | 50 +++++++++++++++++++++++++++++++++ docker-compose.prod.yml | 2 ++ 5 files changed, 61 insertions(+) create mode 100644 backend/prisma/seed.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bb0bfbc..5bba8bc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,4 +26,6 @@ jobs: git clean -fd git reset --hard origin/main docker compose -f docker-compose.prod.yml up -d --build + 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 diff --git a/backend/.env.production.example b/backend/.env.production.example index 758937c..a0b0cea 100644 --- a/backend/.env.production.example +++ b/backend/.env.production.example @@ -13,3 +13,7 @@ LOG_LEVEL=info # Must be at least 32 characters JWT_SECRET=replace-with-a-very-long-random-secret JWT_EXPIRES_IN=7d + +# Default admin account (created on first seed run) +ADMIN_EMAIL=haphantran@gmail.com +ADMIN_PASSWORD=replace-with-a-strong-password diff --git a/backend/package.json b/backend/package.json index 2afda1d..9dd0005 100644 --- a/backend/package.json +++ b/backend/package.json @@ -55,6 +55,9 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.3.3" }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, "engines": { "node": ">=18.0.0" }, diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..777f473 --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,50 @@ +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'haphantran@gmail.com'; +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; + +async function main() { + if (!ADMIN_PASSWORD) { + console.log('ADMIN_PASSWORD not set, skipping admin seed.'); + return; + } + + const existing = await prisma.user.findUnique({ + where: { email: ADMIN_EMAIL.toLowerCase() }, + }); + + 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; + } + + const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 12); + + await prisma.user.create({ + data: { + email: ADMIN_EMAIL.toLowerCase(), + password: hashedPassword, + role: 'ADMIN', + }, + }); + + console.log(`Created admin user: ${ADMIN_EMAIL}`); +} + +main() + .catch((e) => { + console.error('Seed error:', e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 9c413e1..53a1cc5 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,6 +33,8 @@ services: LOG_LEVEL: info JWT_SECRET: ${JWT_SECRET} JWT_EXPIRES_IN: 7d + ADMIN_EMAIL: ${ADMIN_EMAIL:-haphantran@gmail.com} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} # No ports exposed to host — frontend nginx proxies /api/* internally frontend: From 8b2d999efc85e69666f6b6208cb287e8ef52e45b Mon Sep 17 00:00:00 2001 From: Ha Phan Tran Date: Mon, 30 Mar 2026 15:40:24 -0400 Subject: [PATCH 2/2] refactor: use GitHub secrets for admin credentials instead of hardcoding - 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 --- .github/workflows/deploy.yml | 8 ++++++-- backend/prisma/seed.ts | 6 +++--- docker-compose.prod.yml | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5bba8bc..4830b59 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,6 +19,7 @@ jobs: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} + envs: ADMIN_EMAIL,ADMIN_PASSWORD script: | set -e cd /opt/spatialdsl @@ -26,6 +27,9 @@ jobs: git clean -fd git reset --hard origin/main docker compose -f docker-compose.prod.yml up -d --build - 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 compose -f docker-compose.prod.yml exec -T -e ADMIN_EMAIL="$ADMIN_EMAIL" -e ADMIN_PASSWORD="$ADMIN_PASSWORD" backend npx prisma migrate deploy + docker compose -f docker-compose.prod.yml exec -T -e ADMIN_EMAIL="$ADMIN_EMAIL" -e ADMIN_PASSWORD="$ADMIN_PASSWORD" backend npx prisma db seed docker image prune -f + env: + ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }} + ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 777f473..4972e9b 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -3,12 +3,12 @@ import bcrypt from 'bcryptjs'; const prisma = new PrismaClient(); -const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'haphantran@gmail.com'; +const ADMIN_EMAIL = process.env.ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; async function main() { - if (!ADMIN_PASSWORD) { - console.log('ADMIN_PASSWORD not set, skipping admin seed.'); + if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { + console.log('ADMIN_EMAIL or ADMIN_PASSWORD not set, skipping admin seed.'); return; } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 53a1cc5..b3fc75d 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,7 +33,7 @@ services: LOG_LEVEL: info JWT_SECRET: ${JWT_SECRET} JWT_EXPIRES_IN: 7d - ADMIN_EMAIL: ${ADMIN_EMAIL:-haphantran@gmail.com} + ADMIN_EMAIL: ${ADMIN_EMAIL:-} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} # No ports exposed to host — frontend nginx proxies /api/* internally