import { json } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit'; import { getUser, updateUser as dbUpdateUser, deleteUser as dbDeleteUser, deleteUserSessions, countAdminUsers, updateAuthSettings, userHasAdminRole, getRoleByName, assignUserRole, removeUserRole } from '$lib/server/db'; import { hashPassword } from '$lib/server/auth'; import { authorize } from '$lib/server/authorize'; // GET /api/users/[id] - Get a specific user // Free for all - local users are needed for basic auth export const GET: RequestHandler = async ({ params, cookies }) => { const auth = await authorize(cookies); // When auth is enabled, require authentication (any authenticated user can view) if (auth.authEnabled && !auth.isAuthenticated) { return json({ error: 'Authentication required' }, { status: 401 }); } if (!params.id) { return json({ error: 'User ID is required' }, { status: 400 }); } try { const id = parseInt(params.id); const user = await getUser(id); if (!user) { return json({ error: 'User not found' }, { status: 404 }); } // Derive isAdmin from role assignment const isAdmin = await userHasAdminRole(id); return json({ id: user.id, username: user.username, email: user.email, displayName: user.displayName, mfaEnabled: user.mfaEnabled, isAdmin, isActive: user.isActive, lastLogin: user.lastLogin, createdAt: user.createdAt, updatedAt: user.updatedAt }); } catch (error) { console.error('Failed to get user:', error); return json({ error: 'Failed to get user' }, { status: 500 }); } }; // PUT /api/users/[id] - Update a user // Free for all - local users are needed for basic auth export const PUT: RequestHandler = async ({ params, request, cookies }) => { const auth = await authorize(cookies); if (!params.id) { return json({ error: 'User ID is required' }, { status: 400 }); } const userId = parseInt(params.id); // Allow users to edit their own profile, otherwise check permission // (free edition allows all, enterprise checks RBAC) if (auth.user && auth.user.id !== userId && !await auth.can('users', 'edit')) { return json({ error: 'Permission denied' }, { status: 403 }); } try { const data = await request.json(); const existingUser = await getUser(userId); if (!existingUser) { return json({ error: 'User not found' }, { status: 404 }); } // Check if user is currently an admin (via role) const existingUserIsAdmin = await userHasAdminRole(userId); // Build update object const updateData: any = {}; if (data.username !== undefined) updateData.username = data.username; if (data.email !== undefined) updateData.email = data.email; if (data.displayName !== undefined) updateData.displayName = data.displayName; // Track if we need to change admin role let shouldPromote = false; let shouldDemote = false; // Only admins can change admin status or active status if (auth.isAdmin) { // Check if trying to demote or deactivate the last admin if (existingUserIsAdmin) { const adminCount = await countAdminUsers(); const isDemoting = data.isAdmin === false; const isDeactivating = data.isActive === false; if (adminCount <= 1 && (isDemoting || isDeactivating)) { const confirmDisableAuth = data.confirmDisableAuth === true; if (!confirmDisableAuth) { return json({ error: 'This is the last admin user', isLastAdmin: true, message: isDemoting ? 'Removing admin privileges from this user will disable authentication.' : 'Deactivating this user will disable authentication.' }, { status: 409 }); } // User confirmed - proceed and disable auth if (isDemoting) shouldDemote = true; if (isDeactivating) { updateData.isActive = false; await deleteUserSessions(userId); } // Disable authentication await updateAuthSettings({ authEnabled: false }); // Update user first const user = await dbUpdateUser(userId, updateData); if (!user) { return json({ error: 'Failed to update user' }, { status: 500 }); } // Remove Admin role if demoting if (shouldDemote) { const adminRole = await getRoleByName('Admin'); if (adminRole) { await removeUserRole(userId, adminRole.id, null); } } return json({ id: user.id, username: user.username, email: user.email, displayName: user.displayName, mfaEnabled: user.mfaEnabled, isAdmin: !shouldDemote && existingUserIsAdmin, isActive: user.isActive, lastLogin: user.lastLogin, createdAt: user.createdAt, updatedAt: user.updatedAt, authDisabled: true }); } } // Handle isAdmin change via Admin role assignment if (data.isAdmin !== undefined) { if (data.isAdmin && !existingUserIsAdmin) { shouldPromote = true; } else if (!data.isAdmin && existingUserIsAdmin) { shouldDemote = true; } } if (data.isActive !== undefined) { updateData.isActive = data.isActive; // If deactivating, invalidate all sessions if (!data.isActive) { await deleteUserSessions(userId); } } } // Handle password change if (data.password) { if (data.password.length < 8) { return json({ error: 'Password must be at least 8 characters' }, { status: 400 }); } updateData.passwordHash = await hashPassword(data.password); // Invalidate all sessions on password change (except current) await deleteUserSessions(userId); } const user = await dbUpdateUser(userId, updateData); if (!user) { return json({ error: 'Failed to update user' }, { status: 500 }); } // Handle Admin role assignment/removal const adminRole = await getRoleByName('Admin'); if (adminRole) { if (shouldPromote) { await assignUserRole(userId, adminRole.id, null); } else if (shouldDemote) { await removeUserRole(userId, adminRole.id, null); } } // Compute final isAdmin status const finalIsAdmin = shouldPromote || (existingUserIsAdmin && !shouldDemote); return json({ id: user.id, username: user.username, email: user.email, displayName: user.displayName, mfaEnabled: user.mfaEnabled, isAdmin: finalIsAdmin, isActive: user.isActive, lastLogin: user.lastLogin, createdAt: user.createdAt, updatedAt: user.updatedAt }); } catch (error: any) { console.error('Failed to update user:', error); if (error.message?.includes('UNIQUE constraint failed')) { return json({ error: 'Username already exists' }, { status: 409 }); } return json({ error: 'Failed to update user' }, { status: 500 }); } }; // DELETE /api/users/[id] - Delete a user // Free for all - local users are needed for basic auth export const DELETE: RequestHandler = async ({ params, url, cookies }) => { const auth = await authorize(cookies); // When auth is enabled, check permission (free edition allows all, enterprise checks RBAC) if (auth.authEnabled && auth.isAuthenticated && !await auth.can('users', 'remove')) { return json({ error: 'Permission denied' }, { status: 403 }); } if (!params.id) { return json({ error: 'User ID is required' }, { status: 400 }); } const confirmDisableAuth = url.searchParams.get('confirmDisableAuth') === 'true'; try { const id = parseInt(params.id); const isSelfDeletion = auth.user && auth.user.id === id; const user = await getUser(id); if (!user) { return json({ error: 'User not found' }, { status: 404 }); } // Check if user is admin via role const userIsAdmin = await userHasAdminRole(id); // Check if this is the last admin user AND auth is enabled // Only warn if auth is currently ON (deleting last admin will turn it off) if (auth.authEnabled && userIsAdmin) { const adminCount = await countAdminUsers(); if (adminCount <= 1) { // This is the last admin - require confirmation (whether self or other) if (!confirmDisableAuth) { return json({ error: 'This is the last admin user', isLastAdmin: true, isSelf: isSelfDeletion, message: isSelfDeletion ? 'This is the only admin account. Deleting it will disable authentication and allow anyone to access Dockhand.' : 'This is the only admin account. Deleting it will disable authentication and allow anyone to access Dockhand.' }, { status: 409 }); } // User confirmed - proceed with deletion and disable auth await deleteUserSessions(id); const deleted = await dbDeleteUser(id); if (!deleted) { return json({ error: 'Failed to delete user' }, { status: 500 }); } // Disable authentication await updateAuthSettings({ authEnabled: false }); return json({ success: true, authDisabled: true }); } } // Delete all sessions first await deleteUserSessions(id); const deleted = await dbDeleteUser(id); if (!deleted) { return json({ error: 'Failed to delete user' }, { status: 500 }); } return json({ success: true }); } catch (error) { console.error('Failed to delete user:', error); return json({ error: 'Failed to delete user' }, { status: 500 }); } };