72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { authenticateUser, createSession } from '@/lib/auth'
|
|
import { logAuditEvent, needsFirstTimeSetup } from '@/lib/db'
|
|
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
|
|
import { loginLimiter } from '@/lib/rate-limit'
|
|
import { logger } from '@/lib/logger'
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const rateCheck = loginLimiter(request)
|
|
if (rateCheck) return rateCheck
|
|
|
|
const { username, password } = await request.json()
|
|
|
|
if (!username || !password) {
|
|
return NextResponse.json({ error: 'Username and password are required' }, { status: 400 })
|
|
}
|
|
|
|
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
|
const userAgent = request.headers.get('user-agent') || undefined
|
|
|
|
const user = authenticateUser(username, password)
|
|
if (!user) {
|
|
logAuditEvent({ action: 'login_failed', actor: username, ip_address: ipAddress, user_agent: userAgent })
|
|
|
|
// When no users exist at all, give actionable feedback instead of "Invalid credentials"
|
|
if (needsFirstTimeSetup()) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'No admin account has been created yet',
|
|
code: 'NO_USERS',
|
|
hint: 'Visit /setup to create your admin account',
|
|
},
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
|
|
}
|
|
|
|
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent, user.workspace_id)
|
|
|
|
logAuditEvent({ action: 'login', actor: user.username, actor_id: user.id, ip_address: ipAddress, user_agent: userAgent })
|
|
|
|
const response = NextResponse.json({
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
display_name: user.display_name,
|
|
role: user.role,
|
|
provider: user.provider || 'local',
|
|
email: user.email || null,
|
|
avatar_url: user.avatar_url || null,
|
|
workspace_id: user.workspace_id ?? 1,
|
|
tenant_id: user.tenant_id ?? 1,
|
|
},
|
|
})
|
|
|
|
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 }, 'Login error')
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
}
|
|
}
|