Expand ISAC user operations
This commit is contained in:
@@ -8,11 +8,32 @@ import type { Actions, PageServerLoad } from './$types';
|
||||
import { avatarUrl } from '$lib/avatar';
|
||||
import { deleteUploadedAvatar } from '$lib/server/avatars';
|
||||
import { canRemoveAuthenticationMethod, getAuthenticationMethods } from '$lib/server/auth/methods';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, params, url }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), 'users.view');
|
||||
const database = await getDatabase();
|
||||
const [profile, roles, permissions, assignedRoles, directPermissions, logins, builds, comments, authenticationMethods] = await Promise.all([
|
||||
const loginQuery = url.searchParams.get('loginQ')?.trim() ?? '';
|
||||
const loginStatus = ['success', 'failed'].includes(url.searchParams.get('loginStatus') ?? '') ? String(url.searchParams.get('loginStatus')) : '';
|
||||
const buildQuery = url.searchParams.get('buildQ')?.trim() ?? '';
|
||||
const buildVisibility = ['draft', 'private', 'public'].includes(url.searchParams.get('buildVisibility') ?? '') ? String(url.searchParams.get('buildVisibility')) : '';
|
||||
const commentQuery = url.searchParams.get('commentQ')?.trim() ?? '';
|
||||
const pattern = (value: string) => `%${value.replaceAll('%', '\\%').replaceAll('_', '\\_')}%`;
|
||||
const loginWhere = `user_id=? AND provider LIKE ? ESCAPE '\\'${loginStatus ? ' AND succeeded=?' : ''}`;
|
||||
const loginArgs: unknown[] = [params.id, pattern(loginQuery), ...(loginStatus ? [loginStatus === 'success' ? 1 : 0] : [])];
|
||||
const buildWhere = `owner_id=? AND (title LIKE ? ESCAPE '\\' COLLATE NOCASE OR slug LIKE ? ESCAPE '\\' COLLATE NOCASE)${buildVisibility ? ' AND visibility=?' : ''}`;
|
||||
const buildArgs: unknown[] = [params.id, pattern(buildQuery), pattern(buildQuery), ...(buildVisibility ? [buildVisibility] : [])];
|
||||
const commentWhere = `comments.user_id=? AND (comments.body LIKE ? ESCAPE '\\' COLLATE NOCASE OR builds.title LIKE ? ESCAPE '\\' COLLATE NOCASE)`;
|
||||
const commentArgs: unknown[] = [params.id, pattern(commentQuery), pattern(commentQuery)];
|
||||
const [loginCount, buildCount, commentCount] = await Promise.all([
|
||||
(await database.prepare(`SELECT COUNT(*) AS count FROM login_events WHERE ${loginWhere}`)).get(...loginArgs),
|
||||
(await database.prepare(`SELECT COUNT(*) AS count FROM builds WHERE ${buildWhere}`)).get(...buildArgs),
|
||||
(await database.prepare(`SELECT COUNT(*) AS count FROM comments JOIN builds ON builds.id=comments.build_id WHERE ${commentWhere}`)).get(...commentArgs)
|
||||
]);
|
||||
const loginPagination = getPagination(url, Number(loginCount?.count ?? 0), 'login');
|
||||
const buildPagination = getPagination(url, Number(buildCount?.count ?? 0), 'build');
|
||||
const commentPagination = getPagination(url, Number(commentCount?.count ?? 0), 'comment');
|
||||
const [profile, roles, permissions, assignedRoles, directPermissions, logins, builds, comments, authenticationMethods, emails, passkeys] = await Promise.all([
|
||||
(await database.prepare(`SELECT users.id,users.login_username AS username,COALESCE(user_profiles.display_name,users.username) AS display_name,
|
||||
users.role,users.discord_id,users.created_at,
|
||||
CASE WHEN users.password_hash IS NOT NULL THEN 1 ELSE 0 END AS has_password,
|
||||
@@ -30,11 +51,13 @@ export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
(await database.prepare('SELECT * FROM permissions ORDER BY name')).all(),
|
||||
(await database.prepare('SELECT role_id FROM user_roles WHERE user_id=?')).all(params.id),
|
||||
(await database.prepare('SELECT permission_id FROM user_permissions WHERE user_id=?')).all(params.id),
|
||||
(await database.prepare('SELECT provider,succeeded,created_at FROM login_events WHERE user_id=? ORDER BY created_at DESC LIMIT 30')).all(params.id),
|
||||
(await database.prepare('SELECT id,title,slug,visibility,updated_at FROM builds WHERE owner_id=? ORDER BY updated_at DESC LIMIT 50')).all(params.id),
|
||||
(await database.prepare(`SELECT provider,succeeded,created_at FROM login_events WHERE ${loginWhere} ORDER BY created_at DESC LIMIT ? OFFSET ?`)).all(...loginArgs, loginPagination.perPage, loginPagination.offset),
|
||||
(await database.prepare(`SELECT id,title,slug,visibility,updated_at FROM builds WHERE ${buildWhere} ORDER BY updated_at DESC LIMIT ? OFFSET ?`)).all(...buildArgs, buildPagination.perPage, buildPagination.offset),
|
||||
(await database.prepare(`SELECT comments.id,comments.body,comments.created_at,builds.title,builds.slug
|
||||
FROM comments JOIN builds ON builds.id=comments.build_id WHERE comments.user_id=? ORDER BY comments.created_at DESC LIMIT 50`)).all(params.id)
|
||||
FROM comments JOIN builds ON builds.id=comments.build_id WHERE ${commentWhere} ORDER BY comments.created_at DESC LIMIT ? OFFSET ?`)).all(...commentArgs, commentPagination.perPage, commentPagination.offset)
|
||||
,getAuthenticationMethods(database, params.id)
|
||||
,(await database.prepare('SELECT email,verified_at,updated_at FROM user_emails WHERE user_id=?')).all(params.id)
|
||||
,(await database.prepare('SELECT id,name,device_type,backed_up,last_used_at,created_at FROM passkeys WHERE user_id=? ORDER BY created_at DESC')).all(params.id)
|
||||
]);
|
||||
if (!profile) error(404, 'User not found');
|
||||
return {
|
||||
@@ -46,7 +69,9 @@ export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
permissions,
|
||||
assignedRoleIds: assignedRoles.map((row) => String(row.role_id)),
|
||||
directPermissionIds: directPermissions.map((row) => String(row.permission_id)),
|
||||
logins, builds, comments,
|
||||
logins, builds, comments, emails, passkeys,
|
||||
historyFilters: { loginQuery, loginStatus, buildQuery, buildVisibility, commentQuery },
|
||||
historyPagination: { login: loginPagination, build: buildPagination, comment: commentPagination },
|
||||
authenticationMethods,
|
||||
operatorPermissions: await getPermissions(operator),
|
||||
isSelf: operator.id === params.id
|
||||
@@ -159,5 +184,33 @@ export const actions: Actions = {
|
||||
await (await database.prepare('INSERT INTO audit_log(id,actor_id,entity_type,entity_id,action,before_json,created_at) VALUES(?,?,?,?,?,?,?)'))
|
||||
.run(randomUUID(), operator.id, 'user', params.id, 'identity.discord_unlinked', JSON.stringify({ discordId: target.discord_id }), now);
|
||||
return { identityUnlinked: true };
|
||||
},
|
||||
removeEmail: async ({ cookies, params }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), 'users.identities');
|
||||
const database = await getDatabase();
|
||||
const email = await (await database.prepare('SELECT email FROM user_emails WHERE user_id=?')).get(params.id);
|
||||
if (!email) return fail(409, { identityError: 'This user does not have a verified email sign-in.' });
|
||||
if (!canRemoveAuthenticationMethod(await getAuthenticationMethods(database, params.id), 'email')) {
|
||||
return fail(400, { identityError: 'Email cannot be removed because it is this user’s only sign-in method.' });
|
||||
}
|
||||
await (await database.prepare('DELETE FROM user_emails WHERE user_id=?')).run(params.id);
|
||||
await (await database.prepare('DELETE FROM magic_link_tokens WHERE user_id=?')).run(params.id);
|
||||
await (await database.prepare('INSERT INTO audit_log(id,actor_id,entity_type,entity_id,action,before_json,created_at) VALUES(?,?,?,?,?,?,?)'))
|
||||
.run(randomUUID(), operator.id, 'user', params.id, 'identity.email_removed', JSON.stringify({ email: email.email }), new Date().toISOString());
|
||||
return { emailRemoved: true };
|
||||
},
|
||||
removePasskey: async ({ cookies, request, params }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), 'users.identities');
|
||||
const passkeyId = String((await request.formData()).get('passkeyId') ?? '');
|
||||
const database = await getDatabase();
|
||||
const passkey = await (await database.prepare('SELECT id,name FROM passkeys WHERE id=? AND user_id=?')).get(passkeyId, params.id);
|
||||
if (!passkey) return fail(404, { identityError: 'That passkey was not found for this user.' });
|
||||
if (!canRemoveAuthenticationMethod(await getAuthenticationMethods(database, params.id), 'passkey')) {
|
||||
return fail(400, { identityError: 'This passkey cannot be removed because it is the user’s only sign-in method.' });
|
||||
}
|
||||
await (await database.prepare('DELETE FROM passkeys WHERE id=? AND user_id=?')).run(passkeyId, params.id);
|
||||
await (await database.prepare('INSERT INTO audit_log(id,actor_id,entity_type,entity_id,action,before_json,created_at) VALUES(?,?,?,?,?,?,?)'))
|
||||
.run(randomUUID(), operator.id, 'user', params.id, 'identity.passkey_removed', JSON.stringify({ id: passkey.id, name: passkey.name }), new Date().toISOString());
|
||||
return { passkeyRemoved: true };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user