mirror of
https://github.com/BradNut/example-sveltekit-email-password-webauthn
synced 2025-09-08 17:40:27 +00:00
277 lines
7.5 KiB
TypeScript
277 lines
7.5 KiB
TypeScript
import {
|
|
createEmailVerificationRequest,
|
|
sendVerificationEmail,
|
|
sendVerificationEmailBucket,
|
|
setEmailVerificationRequestCookie
|
|
} from "$lib/server/email-verification";
|
|
import { fail, redirect } from "@sveltejs/kit";
|
|
import { checkEmailAvailability, verifyEmailInput } from "$lib/server/email";
|
|
import { verifyPasswordHash, verifyPasswordStrength } from "$lib/server/password";
|
|
import { getUserPasswordHash, getUserRecoverCode, resetUserRecoveryCode, updateUserPassword } from "$lib/server/user";
|
|
import {
|
|
createSession,
|
|
generateSessionToken,
|
|
invalidateUserSessions,
|
|
setSessionTokenCookie
|
|
} from "$lib/server/session";
|
|
import {
|
|
deleteUserPasskeyCredential,
|
|
deleteUserSecurityKeyCredential,
|
|
getUserPasskeyCredentials,
|
|
getUserSecurityKeyCredentials
|
|
} from "$lib/server/webauthn";
|
|
import { decodeBase64 } from "@oslojs/encoding";
|
|
import { get2FARedirect } from "$lib/server/2fa";
|
|
import { deleteUserTOTPKey, totpUpdateBucket } from "$lib/server/totp";
|
|
|
|
import type { Actions, RequestEvent } from "./$types";
|
|
import type { SessionFlags } from "$lib/server/session";
|
|
|
|
export async function load(event: RequestEvent) {
|
|
if (event.locals.user === null || event.locals.session === null) {
|
|
return redirect(302, "/login");
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return redirect(302, get2FARedirect(event.locals.user));
|
|
}
|
|
let recoveryCode: string | null = null;
|
|
if (event.locals.user.registered2FA) {
|
|
recoveryCode = getUserRecoverCode(event.locals.user.id);
|
|
}
|
|
const passkeyCredentials = getUserPasskeyCredentials(event.locals.user.id);
|
|
const securityKeyCredentials = getUserSecurityKeyCredentials(event.locals.user.id);
|
|
return {
|
|
recoveryCode,
|
|
user: event.locals.user,
|
|
passkeyCredentials,
|
|
securityKeyCredentials
|
|
};
|
|
}
|
|
|
|
export const actions: Actions = {
|
|
update_password: updatePasswordAction,
|
|
update_email: updateEmailAction,
|
|
disconnect_totp: disconnectTOTPAction,
|
|
delete_passkey: deletePasskeyAction,
|
|
delete_security_key: deleteSecurityKeyAction,
|
|
regenerate_recovery_code: regenerateRecoveryCodeAction
|
|
};
|
|
|
|
async function updatePasswordAction(event: RequestEvent) {
|
|
if (event.locals.user === null || event.locals.session === null) {
|
|
return fail(401, {
|
|
password: {
|
|
message: "Not authenticated"
|
|
}
|
|
});
|
|
}
|
|
if (!event.locals.user.emailVerified) {
|
|
return fail(403, {
|
|
password: {
|
|
message: "Forbidden"
|
|
}
|
|
});
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return fail(403, {
|
|
password: {
|
|
message: "Forbidden"
|
|
}
|
|
});
|
|
}
|
|
const formData = await event.request.formData();
|
|
const password = formData.get("password");
|
|
const newPassword = formData.get("new_password");
|
|
if (typeof password !== "string" || typeof newPassword !== "string") {
|
|
return fail(400, {
|
|
password: {
|
|
message: "Invalid or missing fields"
|
|
}
|
|
});
|
|
}
|
|
const strongPassword = await verifyPasswordStrength(newPassword);
|
|
if (!strongPassword) {
|
|
return fail(400, {
|
|
password: {
|
|
message: "Weak password"
|
|
}
|
|
});
|
|
}
|
|
const passwordHash = getUserPasswordHash(event.locals.user.id);
|
|
const validPassword = await verifyPasswordHash(passwordHash, password);
|
|
if (!validPassword) {
|
|
return fail(400, {
|
|
password: {
|
|
message: "Incorrect password"
|
|
}
|
|
});
|
|
}
|
|
invalidateUserSessions(event.locals.user.id);
|
|
await updateUserPassword(event.locals.user.id, newPassword);
|
|
|
|
const sessionToken = generateSessionToken();
|
|
const sessionFlags: SessionFlags = {
|
|
twoFactorVerified: event.locals.session.twoFactorVerified
|
|
};
|
|
const session = createSession(sessionToken, event.locals.user.id, sessionFlags);
|
|
setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
|
return {
|
|
password: {
|
|
message: "Updated password"
|
|
}
|
|
};
|
|
}
|
|
|
|
async function updateEmailAction(event: RequestEvent) {
|
|
if (event.locals.session === null || event.locals.user === null) {
|
|
return fail(401, {
|
|
email: {
|
|
message: "Not authenticated"
|
|
}
|
|
});
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return fail(403, {
|
|
email: {
|
|
message: "Forbidden"
|
|
}
|
|
});
|
|
}
|
|
if (!sendVerificationEmailBucket.check(event.locals.user.id, 1)) {
|
|
return fail(429, {
|
|
email: {
|
|
message: "Too many requests"
|
|
}
|
|
});
|
|
}
|
|
|
|
const formData = await event.request.formData();
|
|
const email = formData.get("email");
|
|
if (typeof email !== "string") {
|
|
return fail(400, {
|
|
email: {
|
|
message: "Invalid or missing fields"
|
|
}
|
|
});
|
|
}
|
|
if (email === "") {
|
|
return fail(400, {
|
|
email: {
|
|
message: "Please enter your email"
|
|
}
|
|
});
|
|
}
|
|
if (!verifyEmailInput(email)) {
|
|
return fail(400, {
|
|
email: {
|
|
message: "Please enter a valid email"
|
|
}
|
|
});
|
|
}
|
|
const emailAvailable = checkEmailAvailability(email);
|
|
if (!emailAvailable) {
|
|
return fail(400, {
|
|
email: {
|
|
message: "This email is already used"
|
|
}
|
|
});
|
|
}
|
|
if (!sendVerificationEmailBucket.consume(event.locals.user.id, 1)) {
|
|
return fail(429, {
|
|
email: {
|
|
message: "Too many requests"
|
|
}
|
|
});
|
|
}
|
|
const verificationRequest = createEmailVerificationRequest(event.locals.user.id, email);
|
|
sendVerificationEmail(verificationRequest.email, verificationRequest.code);
|
|
setEmailVerificationRequestCookie(event, verificationRequest);
|
|
return redirect(302, "/verify-email");
|
|
}
|
|
|
|
async function disconnectTOTPAction(event: RequestEvent) {
|
|
if (event.locals.session === null || event.locals.user === null) {
|
|
return fail(401);
|
|
}
|
|
if (!event.locals.user.emailVerified) {
|
|
return fail(403);
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return fail(403);
|
|
}
|
|
if (!totpUpdateBucket.consume(event.locals.user.id, 1)) {
|
|
return fail(429);
|
|
}
|
|
deleteUserTOTPKey(event.locals.user.id);
|
|
return {};
|
|
}
|
|
|
|
async function deletePasskeyAction(event: RequestEvent) {
|
|
if (event.locals.user === null || event.locals.session === null) {
|
|
return fail(401);
|
|
}
|
|
if (!event.locals.user.emailVerified) {
|
|
return fail(403);
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return fail(403);
|
|
}
|
|
const formData = await event.request.formData();
|
|
const encodedCredentialId = formData.get("credential_id");
|
|
if (typeof encodedCredentialId !== "string") {
|
|
return fail(400);
|
|
}
|
|
let credentialId: Uint8Array;
|
|
try {
|
|
credentialId = decodeBase64(encodedCredentialId);
|
|
} catch {
|
|
return fail(400);
|
|
}
|
|
const deleted = deleteUserPasskeyCredential(event.locals.user.id, credentialId);
|
|
if (!deleted) {
|
|
return fail(400);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function deleteSecurityKeyAction(event: RequestEvent) {
|
|
if (event.locals.user === null || event.locals.session === null) {
|
|
return fail(401);
|
|
}
|
|
if (!event.locals.user.emailVerified) {
|
|
return fail(403);
|
|
}
|
|
if (event.locals.user.registered2FA && !event.locals.session.twoFactorVerified) {
|
|
return fail(403);
|
|
}
|
|
const formData = await event.request.formData();
|
|
const encodedCredentialId = formData.get("credential_id");
|
|
if (typeof encodedCredentialId !== "string") {
|
|
return fail(400);
|
|
}
|
|
let credentialId: Uint8Array;
|
|
try {
|
|
credentialId = decodeBase64(encodedCredentialId);
|
|
} catch {
|
|
return fail(400);
|
|
}
|
|
const deleted = deleteUserSecurityKeyCredential(event.locals.user.id, credentialId);
|
|
if (!deleted) {
|
|
return fail(400);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function regenerateRecoveryCodeAction(event: RequestEvent) {
|
|
if (event.locals.session === null || event.locals.user === null) {
|
|
return fail(401);
|
|
}
|
|
if (!event.locals.user.emailVerified) {
|
|
return fail(403);
|
|
}
|
|
if (!event.locals.session.twoFactorVerified) {
|
|
return fail(403);
|
|
}
|
|
resetUserRecoveryCode(event.locals.session.userId);
|
|
return {};
|
|
}
|