Add passkey and magic link authentication

This commit is contained in:
JB
2026-07-25 01:54:33 -04:00
parent 3c9f443499
commit c3244b0c79
31 changed files with 1017 additions and 58 deletions
+8 -5
View File
@@ -7,12 +7,13 @@ import { randomUUID } from 'node:crypto';
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';
export const load: PageServerLoad = async ({ cookies, params }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'users.view');
const database = await getDatabase();
const [profile, roles, permissions, assignedRoles, directPermissions, logins, builds, comments] = await Promise.all([
(await database.prepare(`SELECT users.id,users.username,COALESCE(user_profiles.display_name,users.username) AS display_name,
const [profile, roles, permissions, assignedRoles, directPermissions, logins, builds, comments, authenticationMethods] = 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,
discord_profiles.username AS discord_username,discord_profiles.display_name AS discord_display_name,
@@ -33,6 +34,7 @@ export const load: PageServerLoad = async ({ cookies, params }) => {
(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 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)
,getAuthenticationMethods(database, params.id)
]);
if (!profile) error(404, 'User not found');
return {
@@ -45,6 +47,7 @@ export const load: PageServerLoad = async ({ cookies, params }) => {
assignedRoleIds: assignedRoles.map((row) => String(row.role_id)),
directPermissionIds: directPermissions.map((row) => String(row.permission_id)),
logins, builds, comments,
authenticationMethods,
operatorPermissions: await getPermissions(operator),
isSelf: operator.id === params.id
};
@@ -144,11 +147,11 @@ export const actions: Actions = {
unlinkDiscord: async ({ cookies, params }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'users.identities');
const database = await getDatabase();
const target = await (await database.prepare('SELECT discord_id,password_hash FROM users WHERE id=?')).get(params.id);
const target = await (await database.prepare('SELECT discord_id FROM users WHERE id=?')).get(params.id);
if (!target) return fail(404, { identityError: 'User not found.' });
if (!target.discord_id) return fail(409, { identityError: 'This user does not have a linked Discord account.' });
if (typeof target.password_hash !== 'string') {
return fail(400, { identityError: 'Discord is this user’s only sign-in method. They must set a password before it can be unlinked.' });
if (!canRemoveAuthenticationMethod(await getAuthenticationMethods(database, params.id), 'discord')) {
return fail(400, { identityError: 'Discord cannot be unlinked because it is this user’s only sign-in method.' });
}
const now = new Date().toISOString();
await (await database.prepare('UPDATE users SET discord_id=NULL WHERE id=?')).run(params.id);