Public Access
feat: Implement API modules for admin, auth, exam, result, and setup functionalities
- Created adminApi.ts for admin-related operations including user management and site settings. - Developed authApi.ts for authentication processes such as registration, login, and profile management. - Added examApi.ts for handling exam CRUD operations. - Introduced resultApi.ts for managing results submission and retrieval. - Established setupApi.ts for initial setup processes and status checks. - Created ErrorBoundary component to handle rendering errors gracefully in the frontend. - Re-exported shared API utilities in index.ts for easier access.
This commit is contained in:
1 parent
62af83ce53
commit
c64cdcd9e3
46 files changed
+2227
-1125
No files matched your search
+52
-105
@@ -1,122 +1,69 @@
|
||||
import { Router } from 'express'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { getDb } from '../db/database.js'
|
||||
import { signToken } from '../utils/tokens.js'
|
||||
import { validateBody } from '../middleware/validate.js'
|
||||
import { asyncHandler } from '../utils/asyncHandler.js'
|
||||
import { AppError } from '../middleware/error.js'
|
||||
import * as userRepo from '../repositories/userRepo.js'
|
||||
import * as settingsRepo from '../repositories/settingsRepo.js'
|
||||
import { setupSchema } from '../validators/setupSchemas.js'
|
||||
import { sendTestEmail } from '../utils/email.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
|
||||
const SALT_ROUNDS = 10
|
||||
|
||||
function getSiteTitle(): string | null {
|
||||
return settingsRepo.getSetting('site_title') ?? null
|
||||
}
|
||||
|
||||
// ─── 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' })
|
||||
}
|
||||
})
|
||||
// GET /api/setup/status
|
||||
router.get(
|
||||
'/status',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const title = getSiteTitle()
|
||||
res.json({
|
||||
completed: !!title,
|
||||
siteTitle: title ?? undefined,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
// ─── Perform Initial Setup ───────────────────────────────
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
if (!needsSetup()) {
|
||||
res.status(403).json({ error: 'Setup has already been completed' })
|
||||
return
|
||||
// POST /api/setup
|
||||
router.post(
|
||||
'/',
|
||||
validateBody(setupSchema),
|
||||
asyncHandler(async (req, res) => {
|
||||
if (getSiteTitle()) {
|
||||
throw new AppError(400, 'Setup already completed')
|
||||
}
|
||||
|
||||
const {
|
||||
if (userRepo.countUsers() > 0) {
|
||||
throw new AppError(400, 'Users already exist')
|
||||
}
|
||||
|
||||
const { email, password, displayName, siteTitle, language } = req.body
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS)
|
||||
|
||||
userRepo.createUser({
|
||||
email,
|
||||
password,
|
||||
passwordHash,
|
||||
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',
|
||||
emailVerified: 1,
|
||||
})
|
||||
|
||||
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' })
|
||||
}
|
||||
})
|
||||
settingsRepo.setSettingsBatch([
|
||||
['site_title', siteTitle.trim()],
|
||||
['site_language', language],
|
||||
['maintenance_mode', 'false'],
|
||||
])
|
||||
|
||||
// Send a test email to the new admin
|
||||
sendTestEmail(email).catch(() => {})
|
||||
|
||||
res.status(201).json({ success: true })
|
||||
})
|
||||
)
|
||||
|
||||
export default router
|
||||
export { needsSetup }
|
||||
Reference in new issue
Block a user