113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { needsFirstTimeSetup } from '@/lib/db'
|
|
import { createUser, createSession } from '@/lib/auth'
|
|
import { logAuditEvent } from '@/lib/db'
|
|
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
|
|
import { logger } from '@/lib/logger'
|
|
|
|
const INSECURE_PASSWORDS = new Set([
|
|
'admin',
|
|
'password',
|
|
'change-me-on-first-login',
|
|
'changeme',
|
|
'testpass123',
|
|
])
|
|
|
|
export async function GET() {
|
|
return NextResponse.json({ needsSetup: needsFirstTimeSetup() })
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
// Only allow setup when no users exist
|
|
if (!needsFirstTimeSetup()) {
|
|
return NextResponse.json(
|
|
{ error: 'Setup has already been completed' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
const body = await request.json()
|
|
const { username, password, displayName } = body as {
|
|
username?: string
|
|
password?: string
|
|
displayName?: string
|
|
}
|
|
|
|
// Validate username
|
|
if (!username || typeof username !== 'string') {
|
|
return NextResponse.json({ error: 'Username is required' }, { status: 400 })
|
|
}
|
|
const trimmedUsername = username.trim().toLowerCase()
|
|
if (trimmedUsername.length < 2 || trimmedUsername.length > 64) {
|
|
return NextResponse.json({ error: 'Username must be 2-64 characters' }, { status: 400 })
|
|
}
|
|
if (!/^[a-z0-9_.-]+$/.test(trimmedUsername)) {
|
|
return NextResponse.json(
|
|
{ error: 'Username can only contain lowercase letters, numbers, dots, hyphens, and underscores' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Validate password
|
|
if (!password || typeof password !== 'string') {
|
|
return NextResponse.json({ error: 'Password is required' }, { status: 400 })
|
|
}
|
|
if (password.length < 12) {
|
|
return NextResponse.json({ error: 'Password must be at least 12 characters' }, { status: 400 })
|
|
}
|
|
if (INSECURE_PASSWORDS.has(password)) {
|
|
return NextResponse.json({ error: 'That password is too common. Choose a stronger one.' }, { status: 400 })
|
|
}
|
|
|
|
// Double-check no users exist (race safety — createUser will also fail on duplicate username)
|
|
if (!needsFirstTimeSetup()) {
|
|
return NextResponse.json(
|
|
{ error: 'Another admin was created while you were setting up' },
|
|
{ status: 409 }
|
|
)
|
|
}
|
|
|
|
const resolvedDisplayName = displayName?.trim() ||
|
|
trimmedUsername.charAt(0).toUpperCase() + trimmedUsername.slice(1)
|
|
|
|
const user = createUser(trimmedUsername, password, resolvedDisplayName, 'admin')
|
|
|
|
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
|
const userAgent = request.headers.get('user-agent') || undefined
|
|
|
|
logAuditEvent({
|
|
action: 'setup_admin_created',
|
|
actor: user.username,
|
|
actor_id: user.id,
|
|
ip_address: ipAddress,
|
|
user_agent: userAgent,
|
|
})
|
|
|
|
logger.info(`First-time setup: admin user "${user.username}" created`)
|
|
|
|
// Auto-login: create session and set cookie
|
|
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent, user.workspace_id)
|
|
|
|
const response = NextResponse.json({
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
display_name: user.display_name,
|
|
role: user.role,
|
|
},
|
|
})
|
|
|
|
const isSecureRequest = isRequestSecure(request)
|
|
const cookieName = getMcSessionCookieName(isSecureRequest)
|
|
response.cookies.set(cookieName, token, {
|
|
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
|
})
|
|
|
|
return response
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'Setup error')
|
|
return NextResponse.json({ error: 'Failed to create admin account' }, { status: 500 })
|
|
}
|
|
}
|