Files
dockhand/routes/api/users/[id]/+server.ts
Jarek Krochmalski 62e3c6439e Initial commit
2025-12-28 21:16:03 +01:00

300 lines
8.9 KiB
TypeScript

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 });
}
};