Public Access
feat: implement setup wizard for initial configuration and user registration
This commit is contained in:
1 parent
f7b93f8612
commit
ad777ef1c3
9 files changed
+576
-8
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { runMigrations } from './migrations.js'
|
||||
@@ -10,6 +11,7 @@ let db: Database.Database | null = null
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (!db) {
|
||||
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true })
|
||||
db = new Database(DB_PATH)
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('foreign_keys = ON')
|
||||
|
||||
@@ -5,6 +5,7 @@ import authRoutes from './routes/auth.js'
|
||||
import examRoutes from './routes/exams.js'
|
||||
import resultRoutes from './routes/results.js'
|
||||
import adminRoutes from './routes/admin.js'
|
||||
import setupRoutes from './routes/setup.js'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -24,6 +25,7 @@ getDb()
|
||||
console.log('✓ Database initialized')
|
||||
|
||||
// Routes
|
||||
app.use('/api/setup', setupRoutes)
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/exams', examRoutes)
|
||||
app.use('/api/results', resultRoutes)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getDb } from '../db/database.js'
|
||||
import { signToken, generateVerificationToken } from '../utils/tokens.js'
|
||||
import { sendVerificationEmail, sendPasswordResetEmail, isEmailConfigured } from '../utils/email.js'
|
||||
import { requireAuth } from '../middleware/auth.js'
|
||||
import { needsSetup } from './setup.js'
|
||||
|
||||
const router = Router()
|
||||
const BCRYPT_ROUNDS = 12
|
||||
@@ -13,6 +14,11 @@ const RESET_EXPIRY_MS = 60 * 60 * 1000 // 1 hour
|
||||
// ─── Register ────────────────────────────────────────────
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
if (needsSetup()) {
|
||||
res.status(403).json({ error: 'Initial setup is required before registration' })
|
||||
return
|
||||
}
|
||||
|
||||
const { email, password, displayName } = req.body
|
||||
|
||||
if (!email || !password || !displayName) {
|
||||
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user