feat: implement setup wizard for initial configuration and user registration

This commit is contained in:
LaurensBartelds committed 2026-04-25 16:48:23 +02:00
1 parent f7b93f8612
commit ad777ef1c3
9 files changed
+576 -8

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
import { Router } from 'express'
import bcrypt from 'bcrypt'
import { getDb } from '../db/database.js'
import { signToken } from '../utils/tokens.js'
const router = Router()
const BCRYPT_ROUNDS = 12
function needsSetup(): boolean {
const db = getDb()
const row = db.prepare('SELECT COUNT(*) AS cnt FROM users').get() as { cnt: number }
return row.cnt === 0
}
// ─── Check Setup Status ──────────────────────────────────
router.get('/status', (_req, res) => {
try {
res.json({ needsSetup: needsSetup() })
} catch (err) {
console.error('Setup status error:', err)
res.status(500).json({ error: 'Internal server error' })
}
})
// ─── Perform Initial Setup ───────────────────────────────
router.post('/', async (req, res) => {
try {
if (!needsSetup()) {
res.status(403).json({ error: 'Setup has already been completed' })
return
}
const {
email,
password,
displayName,
smtpHost,
smtpPort,
smtpUser,
smtpPass,
smtpFrom,
smtpSecure,
appUrl,
} = req.body
// Validate admin credentials
if (!email || !password || !displayName) {
res.status(400).json({ error: 'Email, password, and display name are required' })
return
}
if (typeof email !== 'string' || !email.includes('@') || email.length > 255) {
res.status(400).json({ error: 'Invalid email address' })
return
}
if (typeof password !== 'string' || password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' })
return
}
if (typeof displayName !== 'string' || displayName.trim().length < 1 || displayName.length > 100) {
res.status(400).json({ error: 'Display name is required (max 100 characters)' })
return
}
const db = getDb()
// Create admin user (auto-verified since no SMTP yet)
const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS)
const result = db.prepare(`
INSERT INTO users (email, password_hash, display_name, role, email_verified)
VALUES (?, ?, ?, 'admin', 1)
`).run(email.toLowerCase().trim(), passwordHash, displayName.trim())
const userId = result.lastInsertRowid as number
// Save settings
const settings: Record<string, string> = {
app_url: typeof appUrl === 'string' && appUrl.trim() ? appUrl.trim() : 'http://localhost:5173',
}
if (typeof smtpHost === 'string' && smtpHost.trim()) {
settings.smtp_host = smtpHost.trim()
settings.smtp_port = String(Number(smtpPort) || 587)
settings.smtp_user = typeof smtpUser === 'string' ? smtpUser : ''
settings.smtp_pass = typeof smtpPass === 'string' ? smtpPass : ''
settings.smtp_from = typeof smtpFrom === 'string' && smtpFrom.trim() ? smtpFrom.trim() : 'BrainWave <noreply@brainwave.local>'
settings.smtp_secure = typeof smtpSecure === 'string' ? smtpSecure : 'auto'
}
const upsert = db.prepare(`
INSERT INTO site_settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = unixepoch()
`)
for (const [key, value] of Object.entries(settings)) {
upsert.run(key, value)
}
// Generate JWT and log admin in immediately
const token = signToken({
userId,
email: email.toLowerCase().trim(),
displayName: displayName.trim(),
role: 'admin',
})
res.status(201).json({
message: 'Setup completed successfully',
token,
user: {
id: userId,
email: email.toLowerCase().trim(),
displayName: displayName.trim(),
role: 'admin',
},
})
} catch (err) {
console.error('Setup error:', err)
res.status(500).json({ error: 'Internal server error' })
}
})
export default router
export { needsSetup }