File size: 3,826 Bytes
943fe08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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 })
  }
}