Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ 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
git fetch origin main
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 -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 }}
4 changes: 4 additions & 0 deletions backend/.env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
ADMIN_EMAIL=haphantran@gmail.com
ADMIN_EMAIL=admin@example.com

Copilot uses AI. Check for mistakes.
ADMIN_PASSWORD=replace-with-a-strong-password
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
50 changes: 50 additions & 0 deletions backend/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';

const prisma = new PrismaClient();
Comment on lines +1 to +4
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;

async function main() {
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
console.log('ADMIN_EMAIL or 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;
}
Comment on lines +19 to +30
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.

const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 12);

Comment on lines +32 to +33
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
await prisma.user.create({
data: {
email: ADMIN_EMAIL.toLowerCase(),
password: hashedPassword,
role: 'ADMIN',
},
});
Comment on lines +34 to +40
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

console.log(`Created admin user: ${ADMIN_EMAIL}`);
}

main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
2 changes: 2 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ services:
LOG_LEVEL: info
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 7d
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
# No ports exposed to host — frontend nginx proxies /api/* internally

frontend:
Expand Down