mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Writing code to send recovery to it's own page.
This commit is contained in:
parent
564c58a2c6
commit
4880b87922
8 changed files with 1947 additions and 262 deletions
14
drizzle/0010_wakeful_metal_master.sql
Normal file
14
drizzle/0010_wakeful_metal_master.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "recovery_codes" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"used" boolean DEFAULT false,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "recovery_codes" ADD CONSTRAINT "recovery_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1598
drizzle/meta/0010_snapshot.json
Normal file
1598
drizzle/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -71,6 +71,13 @@
|
||||||
"when": 1711868447607,
|
"when": 1711868447607,
|
||||||
"tag": "0009_gray_carlie_cooper",
|
"tag": "0009_gray_carlie_cooper",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1712271520175,
|
||||||
|
"tag": "0010_wakeful_metal_master",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
24
package.json
24
package.json
|
|
@ -22,15 +22,15 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@melt-ui/pp": "^0.3.0",
|
"@melt-ui/pp": "^0.3.0",
|
||||||
"@melt-ui/svelte": "^0.76.2",
|
"@melt-ui/svelte": "^0.76.3",
|
||||||
"@playwright/test": "^1.42.1",
|
"@playwright/test": "^1.43.0",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@sveltejs/adapter-auto": "^3.2.0",
|
"@sveltejs/adapter-auto": "^3.2.0",
|
||||||
"@sveltejs/enhanced-img": "^0.1.9",
|
"@sveltejs/enhanced-img": "^0.1.9",
|
||||||
"@sveltejs/kit": "^2.5.5",
|
"@sveltejs/kit": "^2.5.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/node": "^20.12.2",
|
"@types/node": "^20.12.4",
|
||||||
"@types/pg": "^8.11.4",
|
"@types/pg": "^8.11.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
|
@ -45,14 +45,14 @@
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-load-config": "^5.0.3",
|
"postcss-load-config": "^5.0.3",
|
||||||
"postcss-preset-env": "^9.5.2",
|
"postcss-preset-env": "^9.5.4",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.2.2",
|
"prettier-plugin-svelte": "^3.2.2",
|
||||||
"sass": "^1.72.0",
|
"sass": "^1.74.1",
|
||||||
"satori": "^0.10.13",
|
"satori": "^0.10.13",
|
||||||
"satori-html": "^0.3.2",
|
"satori-html": "^0.3.2",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.8",
|
"svelte-check": "^3.6.9",
|
||||||
"svelte-headless-table": "^0.18.2",
|
"svelte-headless-table": "^0.18.2",
|
||||||
"svelte-meta-tags": "^3.1.1",
|
"svelte-meta-tags": "^3.1.1",
|
||||||
"svelte-preprocess": "^5.1.3",
|
"svelte-preprocess": "^5.1.3",
|
||||||
|
|
@ -63,9 +63,9 @@
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.6.1",
|
"tslib": "^2.6.1",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.7.2",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.4",
|
||||||
"vite": "^5.2.7",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^1.4.0",
|
"vitest": "^1.4.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"drizzle-orm": "^0.30.6",
|
"drizzle-orm": "^0.30.7",
|
||||||
"feather-icons": "^4.29.1",
|
"feather-icons": "^4.29.1",
|
||||||
"formsnap": "^0.5.1",
|
"formsnap": "^0.5.1",
|
||||||
"html-entities": "^2.5.2",
|
"html-entities": "^2.5.2",
|
||||||
|
|
@ -102,9 +102,9 @@
|
||||||
"loader": "^2.1.1",
|
"loader": "^2.1.1",
|
||||||
"lucia": "3.1.1",
|
"lucia": "3.1.1",
|
||||||
"lucide-svelte": "^0.358.0",
|
"lucide-svelte": "^0.358.0",
|
||||||
"open-props": "^1.6.21",
|
"open-props": "^1.7.0",
|
||||||
"oslo": "^1.2.0",
|
"oslo": "^1.2.0",
|
||||||
"pg": "^8.11.4",
|
"pg": "^8.11.5",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"radix-svelte": "^0.9.0",
|
"radix-svelte": "^0.9.0",
|
||||||
|
|
|
||||||
477
pnpm-lock.yaml
477
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
|
|
@ -6,7 +6,11 @@
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface PageData {
|
interface PageData {
|
||||||
flash?: { type: 'success' | 'error' | 'info'; message: string; data: any };
|
flash?: {
|
||||||
|
type: 'success' | 'error' | 'info';
|
||||||
|
message: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
auth: import('lucia').AuthRequest;
|
auth: import('lucia').AuthRequest;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,29 @@ export const load: PageServerLoad = async (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dbUser?.two_factor_enabled) {
|
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 = {
|
const message = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: 'Two-Factor Authentication is already enabled',
|
message: 'Two-Factor Authentication is already enabled',
|
||||||
|
|
@ -48,7 +71,8 @@ export const load: PageServerLoad = async (event) => {
|
||||||
const accountName = user.email || user.username;
|
const accountName = user.email || user.username;
|
||||||
// pass the website's name and the user identifier (e.g. email, username)
|
// pass the website's name and the user identifier (e.g. email, username)
|
||||||
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
|
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
|
||||||
const qrCode = QRCode.toDataURL(totpUri);
|
const qrCode = await QRCode.toDataURL(totpUri);
|
||||||
|
console.log('QR Code: ', qrCode);
|
||||||
|
|
||||||
form.data = {
|
form.data = {
|
||||||
current_password: '',
|
current_password: '',
|
||||||
|
|
@ -57,6 +81,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
|
recoveryCodes: [],
|
||||||
totpUri,
|
totpUri,
|
||||||
qrCode,
|
qrCode,
|
||||||
};
|
};
|
||||||
|
|
@ -129,19 +154,12 @@ export const actions: Actions = {
|
||||||
|
|
||||||
await db.update(users).set({ two_factor_enabled: true }).where(eq(users.id, user.id));
|
await db.update(users).set({ two_factor_enabled: true }).where(eq(users.id, user.id));
|
||||||
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form.data.current_password = '';
|
form.data.current_password = '';
|
||||||
form.data.two_factor_code = '';
|
form.data.two_factor_code = '';
|
||||||
return { recoveryCodes };
|
return {
|
||||||
|
form,
|
||||||
|
twoFactorEnabled: true,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,49 @@
|
||||||
import { addTwoFactorSchema } from '$lib/validations/account';
|
import { addTwoFactorSchema } from '$lib/validations/account';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
export let form;
|
|
||||||
|
|
||||||
console.log('recovery codes', form.recoveryCodes)
|
const { qrCode, twoFactorEnabled, recoveryCodes } = data;
|
||||||
|
|
||||||
const { qrCode } = data;
|
const form = superForm(data.form, {
|
||||||
|
|
||||||
const twoFactorForm = superForm(data.form, {
|
|
||||||
taintedMessage: null,
|
taintedMessage: null,
|
||||||
validators: zodClient(addTwoFactorSchema),
|
validators: zodClient(addTwoFactorSchema),
|
||||||
delayMs: 500,
|
delayMs: 500,
|
||||||
multipleSubmits: 'prevent',
|
multipleSubmits: 'prevent',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form: formData, enhance } = twoFactorForm;
|
const { form: formData, enhance } = form;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Two-Factor Authentication</h1>
|
<h1>Two-Factor Authentication</h1>
|
||||||
|
|
||||||
<img src={qrCode} alt="QR Code" />
|
{#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.Field {form} name="two_factor_code">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label for="code">Enter Code</Form.Label>
|
||||||
|
<Input {...attrs} bind:value={$formData.two_factor_code} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description>This is the code from your authenticator app.</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field {form} name="current_password">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label for="password">Enter Password</Form.Label>
|
||||||
|
<Input type="password" {...attrs} bind:value={$formData.current_password} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description>Please enter your current password.</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Button>Submit</Form.Button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
Loading…
Reference in a new issue