mission-control/src/app/api/auth/me/route.ts

115 lines
3.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getUserFromRequest, updateUser, requireRole } from '@/lib/auth'
import { logAuditEvent } from '@/lib/db'
import { verifyPassword } from '@/lib/password'
import { logger } from '@/lib/logger'
export async function GET(request: Request) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const user = getUserFromRequest(request)
if (!user) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
}
return 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,
},
})
}
/**
* PATCH /api/auth/me - Self-service password change and display name update.
* Body: { current_password, new_password } and/or { display_name }
*/
export async function PATCH(request: NextRequest) {
const user = getUserFromRequest(request)
if (!user) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
}
// API key users (id=0) cannot change passwords
if (user.id === 0) {
return NextResponse.json({ error: 'API key users cannot change passwords' }, { status: 403 })
}
try {
const { current_password, new_password, display_name } = await request.json()
const updates: { password?: string; display_name?: string } = {}
// Handle password change
if (new_password) {
if (!current_password) {
return NextResponse.json({ error: 'Current password is required' }, { status: 400 })
}
if (new_password.length < 8) {
return NextResponse.json({ error: 'New password must be at least 8 characters' }, { status: 400 })
}
// Verify current password by fetching stored hash
const { getDatabase } = await import('@/lib/db')
const db = getDatabase()
const row = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(user.id) as any
if (!row || !verifyPassword(current_password, row.password_hash)) {
return NextResponse.json({ error: 'Current password is incorrect' }, { status: 403 })
}
updates.password = new_password
}
// Handle display name update
if (display_name !== undefined) {
if (!display_name.trim()) {
return NextResponse.json({ error: 'Display name cannot be empty' }, { status: 400 })
}
updates.display_name = display_name.trim()
}
if (Object.keys(updates).length === 0) {
return NextResponse.json({ error: 'No updates provided' }, { status: 400 })
}
const updated = updateUser(user.id, updates)
if (!updated) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
if (updates.password) {
logAuditEvent({ action: 'password_change', actor: user.username, actor_id: user.id, ip_address: ipAddress })
}
if (updates.display_name) {
logAuditEvent({ action: 'profile_update', actor: user.username, actor_id: user.id, detail: { display_name: updates.display_name }, ip_address: ipAddress })
}
return NextResponse.json({
success: true,
user: {
id: updated.id,
username: updated.username,
display_name: updated.display_name,
role: updated.role,
provider: updated.provider || 'local',
email: updated.email || null,
avatar_url: updated.avatar_url || null,
workspace_id: updated.workspace_id ?? 1,
},
})
} catch (error) {
logger.error({ err: error }, 'PATCH /api/auth/me error')
return NextResponse.json({ error: 'Failed to update profile' }, { status: 500 })
}
}