mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Separate page for two factory recovery codes.
This commit is contained in:
parent
4880b87922
commit
826d06113d
7 changed files with 103 additions and 107 deletions
|
|
@ -30,7 +30,7 @@
|
|||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/pg": "^8.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
"loader": "^2.1.1",
|
||||
"lucia": "3.1.1",
|
||||
"lucide-svelte": "^0.358.0",
|
||||
"open-props": "^1.7.0",
|
||||
"open-props": "^1.7.2",
|
||||
"oslo": "^1.2.0",
|
||||
"pg": "^8.11.5",
|
||||
"postgres": "^3.4.4",
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ dependencies:
|
|||
specifier: ^0.358.0
|
||||
version: 0.358.0(svelte@4.2.12)
|
||||
open-props:
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2
|
||||
oslo:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
|
|
@ -152,8 +152,8 @@ devDependencies:
|
|||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
'@types/node':
|
||||
specifier: ^20.12.4
|
||||
version: 20.12.4
|
||||
specifier: ^20.12.5
|
||||
version: 20.12.5
|
||||
'@types/pg':
|
||||
specifier: ^8.11.4
|
||||
version: 8.11.4
|
||||
|
|
@ -246,7 +246,7 @@ devDependencies:
|
|||
version: 3.4.3(ts-node@10.9.2)
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@20.12.4)(typescript@5.4.4)
|
||||
version: 10.9.2(@types/node@20.12.5)(typescript@5.4.4)
|
||||
tslib:
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.2
|
||||
|
|
@ -258,10 +258,10 @@ devDependencies:
|
|||
version: 5.4.4
|
||||
vite:
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
version: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
vitest:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0(@types/node@20.12.4)(sass@1.74.1)
|
||||
version: 1.4.0(@types/node@20.12.5)(sass@1.74.1)
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
|
|
@ -3286,7 +3286,7 @@ packages:
|
|||
sirv: 2.0.4
|
||||
svelte: 4.2.12
|
||||
tiny-glob: 0.2.9
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
|
||||
/@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8):
|
||||
resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==}
|
||||
|
|
@ -3299,7 +3299,7 @@ packages:
|
|||
'@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.2.8)
|
||||
debug: 4.3.4
|
||||
svelte: 4.2.12
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -3317,7 +3317,7 @@ packages:
|
|||
magic-string: 0.30.5
|
||||
svelte: 4.2.12
|
||||
svelte-hmr: 0.15.3(svelte@4.2.12)
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
vitefu: 0.2.5(vite@5.2.8)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -3360,22 +3360,22 @@ packages:
|
|||
/@types/json-schema@7.0.15:
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
/@types/node@20.12.4:
|
||||
resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==}
|
||||
/@types/node@20.12.5:
|
||||
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
/@types/pg@8.11.4:
|
||||
resolution: {integrity: sha512-yw3Bwbda6vO+NvI1Ue/YKOwtl31AYvvd/e73O3V4ZkNzuGpTDndLSyc0dQRB2xrQqDePd20pEGIfqSp/GH3pRw==}
|
||||
dependencies:
|
||||
'@types/node': 20.12.4
|
||||
'@types/node': 20.12.5
|
||||
pg-protocol: 1.6.0
|
||||
pg-types: 4.0.2
|
||||
|
||||
/@types/pg@8.6.6:
|
||||
resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==}
|
||||
dependencies:
|
||||
'@types/node': 20.12.4
|
||||
'@types/node': 20.12.5
|
||||
pg-protocol: 1.6.0
|
||||
pg-types: 2.2.0
|
||||
dev: false
|
||||
|
|
@ -5851,8 +5851,8 @@ packages:
|
|||
mimic-fn: 4.0.0
|
||||
dev: true
|
||||
|
||||
/open-props@1.7.0:
|
||||
resolution: {integrity: sha512-exvA+8HSxD5qihtBnaDQ1uSKrZV/hfM4/K6UCY7LMzwiMKwejshuNPVpM7exoe74wluOq1NdWYmoSfFxGW9iyw==}
|
||||
/open-props@1.7.2:
|
||||
resolution: {integrity: sha512-RheKypVzZBCSZ6c5iJaFWG0OBqdtql3eRFXRYrSNLh6vGzU8NSAHuq9iJPj++DrpPGs1pqlRa2BelwwBHjX3Xg==}
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.3:
|
||||
|
|
@ -6334,7 +6334,7 @@ packages:
|
|||
dependencies:
|
||||
lilconfig: 2.1.0
|
||||
postcss: 8.4.38
|
||||
ts-node: 10.9.2(@types/node@20.12.4)(typescript@5.4.4)
|
||||
ts-node: 10.9.2(@types/node@20.12.5)(typescript@5.4.4)
|
||||
yaml: 1.10.2
|
||||
dev: true
|
||||
|
||||
|
|
@ -6352,7 +6352,7 @@ packages:
|
|||
dependencies:
|
||||
lilconfig: 3.0.0
|
||||
postcss: 8.4.38
|
||||
ts-node: 10.9.2(@types/node@20.12.4)(typescript@5.4.4)
|
||||
ts-node: 10.9.2(@types/node@20.12.5)(typescript@5.4.4)
|
||||
yaml: 2.3.4
|
||||
|
||||
/postcss-load-config@5.0.3(postcss@8.4.38):
|
||||
|
|
@ -7365,23 +7365,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@sveltejs/kit': 1.x || 2.x
|
||||
svelte: 3.x || 4.x || >=5.0.0-next.51
|
||||
peerDependenciesMeta:
|
||||
'@sinclair/typebox':
|
||||
optional: true
|
||||
'@vinejs/vine':
|
||||
optional: true
|
||||
arktype:
|
||||
optional: true
|
||||
joi:
|
||||
optional: true
|
||||
superstruct:
|
||||
optional: true
|
||||
valibot:
|
||||
optional: true
|
||||
yup:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.5.5(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8)
|
||||
devalue: 4.3.2
|
||||
|
|
@ -7574,7 +7557,7 @@ packages:
|
|||
/ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
/ts-node@10.9.2(@types/node@20.12.4)(typescript@5.4.4):
|
||||
/ts-node@10.9.2(@types/node@20.12.5)(typescript@5.4.4):
|
||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
|
|
@ -7593,7 +7576,7 @@ packages:
|
|||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 20.12.4
|
||||
'@types/node': 20.12.5
|
||||
acorn: 8.11.2
|
||||
acorn-walk: 8.3.0
|
||||
arg: 4.1.3
|
||||
|
|
@ -7742,7 +7725,7 @@ packages:
|
|||
- rollup
|
||||
dev: true
|
||||
|
||||
/vite-node@1.4.0(@types/node@20.12.4)(sass@1.74.1):
|
||||
/vite-node@1.4.0(@types/node@20.12.5)(sass@1.74.1):
|
||||
resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
|
@ -7751,7 +7734,7 @@ packages:
|
|||
debug: 4.3.4
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.0
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
|
|
@ -7763,7 +7746,7 @@ packages:
|
|||
- terser
|
||||
dev: true
|
||||
|
||||
/vite@5.2.8(@types/node@20.12.4)(sass@1.74.1):
|
||||
/vite@5.2.8(@types/node@20.12.5)(sass@1.74.1):
|
||||
resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
|
@ -7791,7 +7774,7 @@ packages:
|
|||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.12.4
|
||||
'@types/node': 20.12.5
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.38
|
||||
rollup: 4.13.0
|
||||
|
|
@ -7807,9 +7790,9 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
|
||||
/vitest@1.4.0(@types/node@20.12.4)(sass@1.74.1):
|
||||
/vitest@1.4.0(@types/node@20.12.5)(sass@1.74.1):
|
||||
resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
|
@ -7834,7 +7817,7 @@ packages:
|
|||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.12.4
|
||||
'@types/node': 20.12.5
|
||||
'@vitest/expect': 1.4.0
|
||||
'@vitest/runner': 1.4.0
|
||||
'@vitest/snapshot': 1.4.0
|
||||
|
|
@ -7852,8 +7835,8 @@ packages:
|
|||
strip-literal: 2.0.0
|
||||
tinybench: 2.6.0
|
||||
tinypool: 0.8.2
|
||||
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite-node: 1.4.0(@types/node@20.12.4)(sass@1.74.1)
|
||||
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)
|
||||
vite-node: 1.4.0(@types/node@20.12.5)(sass@1.74.1)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
return {
|
||||
profileForm,
|
||||
emailForm,
|
||||
hasSetupTwoFactor: !!dbUser?.two_factor_secret,
|
||||
hasSetupTwoFactor: !!dbUser?.two_factor_enabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { type Actions, fail } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { encodeHex, decodeHex } from 'oslo/encoding';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { createTOTPKeyURI, TOTPController } from 'oslo/otp';
|
||||
import { HMAC } from 'oslo/crypto';
|
||||
import QRCode from 'qrcode';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import type { PageServerLoad } from '../../$types';
|
||||
import { addTwoFactorSchema } from '$lib/validations/account';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import db from '$lib/drizzle';
|
||||
import { recovery_codes, users } from '../../../../../../schema';
|
||||
import { users } from '../../../../../../schema';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const form = await superValidate(event, zod(addTwoFactorSchema));
|
||||
|
|
@ -23,41 +22,6 @@ export const load: PageServerLoad = async (event) => {
|
|||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, user.id),
|
||||
});
|
||||
|
||||
if (dbUser?.two_factor_enabled) {
|
||||
const recoveryCodes = await db.query.recovery_codes.findMany({
|
||||
where: eq(recovery_codes.userId, user.id),
|
||||
});
|
||||
|
||||
if (recoveryCodes.length === 0) {
|
||||
const recoveryCodes = generateRecoveryCodes();
|
||||
if (recoveryCodes) {
|
||||
for (const code of recoveryCodes) {
|
||||
await db.insert(recovery_codes).values({
|
||||
userId: user.id,
|
||||
code: await new Argon2id().hash(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
form,
|
||||
twoFactorEnabled: true,
|
||||
recoveryCodes,
|
||||
totpUri: '',
|
||||
qrCode: '',
|
||||
};
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: 'info',
|
||||
message: 'Two-Factor Authentication is already enabled',
|
||||
} as const;
|
||||
throw redirect('/profile', message, event);
|
||||
}
|
||||
|
||||
const twoFactorSecret = await new HMAC('SHA-1').generateKey();
|
||||
await db
|
||||
.update(users)
|
||||
|
|
@ -71,8 +35,6 @@ export const load: PageServerLoad = async (event) => {
|
|||
const accountName = user.email || user.username;
|
||||
// pass the website's name and the user identifier (e.g. email, username)
|
||||
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
|
||||
const qrCode = await QRCode.toDataURL(totpUri);
|
||||
console.log('QR Code: ', qrCode);
|
||||
|
||||
form.data = {
|
||||
current_password: '',
|
||||
|
|
@ -83,7 +45,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
twoFactorEnabled: false,
|
||||
recoveryCodes: [],
|
||||
totpUri,
|
||||
qrCode,
|
||||
qrCode: await QRCode.toDataURL(totpUri),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -154,15 +116,6 @@ export const actions: Actions = {
|
|||
|
||||
await db.update(users).set({ two_factor_enabled: true }).where(eq(users.id, user.id));
|
||||
|
||||
form.data.current_password = '';
|
||||
form.data.two_factor_code = '';
|
||||
return {
|
||||
form,
|
||||
twoFactorEnabled: true,
|
||||
};
|
||||
redirect(302, '/profile/security/two-factor/recovery-codes');
|
||||
},
|
||||
};
|
||||
|
||||
function generateRecoveryCodes() {
|
||||
return Array.from({ length: 5 }, () => cuid2());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
multipleSubmits: 'prevent',
|
||||
});
|
||||
|
||||
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
|
|
@ -25,16 +27,10 @@
|
|||
|
||||
{#if twoFactorEnabled}
|
||||
<h2>Two-Factor Authentication is <span class="text-green-500">enabled</span></h2>
|
||||
{#if recoveryCodes.length > 0}
|
||||
Please copy the recovery codes below as they will not be shown again.
|
||||
{#each recoveryCodes as code}
|
||||
<p>{code}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<h2>Please scan the following QR Code</h2>
|
||||
<img src={qrCode} alt="QR Code" />
|
||||
<form method="POST" use:enhance>
|
||||
<form method="POST" use:enhance data-sveltekit-replacestate>
|
||||
<Form.Field {form} name="two_factor_code">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="code">Enter Code</Form.Label>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import db from "$lib/drizzle";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {Argon2id} from "oslo/password";
|
||||
import {alphabet, generateRandomString} from "oslo/crypto";
|
||||
import {redirect} from "sveltekit-flash-message/server";
|
||||
import {notSignedInMessage} from "$lib/flashMessages";
|
||||
import type { PageServerLoad } from '../../../$types';
|
||||
import {recovery_codes, users} from "../../../../../../../schema";
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const user = event.locals.user;
|
||||
|
||||
if (!user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, user.id),
|
||||
});
|
||||
|
||||
if (dbUser?.two_factor_enabled) {
|
||||
const recoveryCodes = await db.query.recovery_codes.findMany({
|
||||
where: eq(recovery_codes.userId, user.id),
|
||||
});
|
||||
|
||||
if (recoveryCodes.length === 0) {
|
||||
const recoveryCodes = Array.from({length: 5}, () => generateRandomString(10, alphabet('A-Z', '0-9')));
|
||||
if (recoveryCodes) {
|
||||
for (const code of recoveryCodes) {
|
||||
await db.insert(recovery_codes).values({
|
||||
userId: user.id,
|
||||
code: await new Argon2id().hash(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
recoveryCodes,
|
||||
};
|
||||
}
|
||||
return {
|
||||
recoveryCodes: [],
|
||||
}
|
||||
} else {
|
||||
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
export let data;
|
||||
|
||||
const { recoveryCodes } = data;
|
||||
</script>
|
||||
|
||||
<h2>Two-Factor Authentication is <span class="text-green-500">enabled</span></h2>
|
||||
{#if recoveryCodes && recoveryCodes.length > 0}
|
||||
{#if recoveryCodes.length > 0}
|
||||
Please copy the recovery codes below as they will not be shown again.
|
||||
{#each recoveryCodes as code}
|
||||
<p>{code}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<h2>You have already viewed your recovery codes.</h2>
|
||||
<p>If you wish to generate new codes, please disable and then re-enable two-factor authentication.</p>
|
||||
{/if}
|
||||
Loading…
Reference in a new issue