mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-06 13:21:53 +00:00
Initial commit
This commit is contained in:
299
routes/api/users/[id]/+server.ts
Normal file
299
routes/api/users/[id]/+server.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
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 });
|
||||
}
|
||||
};
|
||||
96
routes/api/users/[id]/mfa/+server.ts
Normal file
96
routes/api/users/[id]/mfa/+server.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { isEnterprise } from '$lib/server/license';
|
||||
import {
|
||||
validateSession,
|
||||
checkPermission,
|
||||
generateMfaSetup,
|
||||
verifyAndEnableMfa,
|
||||
disableMfa
|
||||
} from '$lib/server/auth';
|
||||
|
||||
// POST /api/users/[id]/mfa - Setup MFA (generate QR code)
|
||||
export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
|
||||
if (!params.id) {
|
||||
return json({ error: 'User ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const userId = parseInt(params.id);
|
||||
|
||||
// Users can only setup MFA for themselves, or admins can do it for others
|
||||
if (!currentUser || (currentUser.id !== userId && !currentUser.isAdmin)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
|
||||
// Check if this is a verification request
|
||||
if (body.action === 'verify') {
|
||||
if (!body.token) {
|
||||
return json({ error: 'MFA token is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const success = await verifyAndEnableMfa(userId, body.token);
|
||||
if (!success) {
|
||||
return json({ error: 'Invalid MFA code' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ success: true, message: 'MFA enabled successfully' });
|
||||
}
|
||||
|
||||
// Generate new MFA setup
|
||||
const setup = await generateMfaSetup(userId);
|
||||
if (!setup) {
|
||||
return json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
secret: setup.secret,
|
||||
qrDataUrl: setup.qrDataUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('MFA setup error:', error);
|
||||
return json({ error: 'Failed to setup MFA' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/users/[id]/mfa - Disable MFA
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
|
||||
if (!params.id) {
|
||||
return json({ error: 'User ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const userId = parseInt(params.id);
|
||||
|
||||
// Users can only disable their own MFA, or admins can do it for others
|
||||
if (!currentUser || (currentUser.id !== userId && !currentUser.isAdmin)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await disableMfa(userId);
|
||||
if (!success) {
|
||||
return json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ success: true, message: 'MFA disabled successfully' });
|
||||
} catch (error) {
|
||||
console.error('MFA disable error:', error);
|
||||
return json({ error: 'Failed to disable MFA' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
110
routes/api/users/[id]/roles/+server.ts
Normal file
110
routes/api/users/[id]/roles/+server.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { isEnterprise } from '$lib/server/license';
|
||||
import { validateSession } from '$lib/server/auth';
|
||||
import {
|
||||
getUserRoles,
|
||||
assignUserRole,
|
||||
removeUserRole,
|
||||
getUser
|
||||
} from '$lib/server/db';
|
||||
|
||||
// GET /api/users/[id]/roles - Get roles assigned to a user
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!params.id) {
|
||||
return json({ error: 'User ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = parseInt(params.id);
|
||||
const user = await getUser(userId);
|
||||
|
||||
if (!user) {
|
||||
return json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const userRoles = await getUserRoles(userId);
|
||||
return json(userRoles);
|
||||
} catch (error) {
|
||||
console.error('Failed to get user roles:', error);
|
||||
return json({ error: 'Failed to get user roles' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/users/[id]/roles - Assign a role to a user
|
||||
export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
if (!currentUser || !currentUser.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!params.id) {
|
||||
return json({ error: 'User ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = parseInt(params.id);
|
||||
const { roleId, environmentId } = await request.json();
|
||||
|
||||
if (!roleId) {
|
||||
return json({ error: 'Role ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await getUser(userId);
|
||||
if (!user) {
|
||||
return json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const userRole = await assignUserRole(userId, roleId, environmentId);
|
||||
return json(userRole, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Failed to assign role:', error);
|
||||
return json({ error: 'Failed to assign role' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/users/[id]/roles - Remove a role from a user
|
||||
export const DELETE: RequestHandler = async ({ params, request, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
if (!currentUser || !currentUser.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!params.id) {
|
||||
return json({ error: 'User ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = parseInt(params.id);
|
||||
const { roleId, environmentId } = await request.json();
|
||||
|
||||
if (!roleId) {
|
||||
return json({ error: 'Role ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const deleted = await removeUserRole(userId, roleId, environmentId);
|
||||
if (!deleted) {
|
||||
return json({ error: 'Role assignment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to remove role:', error);
|
||||
return json({ error: 'Failed to remove role' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user