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,
|
||||
"tag": "0009_gray_carlie_cooper",
|
||||
"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": {
|
||||
"@melt-ui/pp": "^0.3.0",
|
||||
"@melt-ui/svelte": "^0.76.2",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@melt-ui/svelte": "^0.76.3",
|
||||
"@playwright/test": "^1.43.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.0",
|
||||
"@sveltejs/enhanced-img": "^0.1.9",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.12.2",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/pg": "^8.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
|
|
@ -45,14 +45,14 @@
|
|||
"postcss": "^8.4.38",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-load-config": "^5.0.3",
|
||||
"postcss-preset-env": "^9.5.2",
|
||||
"postcss-preset-env": "^9.5.4",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"sass": "^1.72.0",
|
||||
"sass": "^1.74.1",
|
||||
"satori": "^0.10.13",
|
||||
"satori-html": "^0.3.2",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.8",
|
||||
"svelte-check": "^3.6.9",
|
||||
"svelte-headless-table": "^0.18.2",
|
||||
"svelte-meta-tags": "^3.1.1",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
|
|
@ -63,9 +63,9 @@
|
|||
"tailwindcss": "^3.4.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.7",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "^1.4.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cookie": "^0.6.0",
|
||||
"drizzle-orm": "^0.30.6",
|
||||
"drizzle-orm": "^0.30.7",
|
||||
"feather-icons": "^4.29.1",
|
||||
"formsnap": "^0.5.1",
|
||||
"html-entities": "^2.5.2",
|
||||
|
|
@ -102,9 +102,9 @@
|
|||
"loader": "^2.1.1",
|
||||
"lucia": "3.1.1",
|
||||
"lucide-svelte": "^0.358.0",
|
||||
"open-props": "^1.6.21",
|
||||
"open-props": "^1.7.0",
|
||||
"oslo": "^1.2.0",
|
||||
"pg": "^8.11.4",
|
||||
"pg": "^8.11.5",
|
||||
"postgres": "^3.4.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"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 {
|
||||
namespace App {
|
||||
interface PageData {
|
||||
flash?: { type: 'success' | 'error' | 'info'; message: string; data: any };
|
||||
flash?: {
|
||||
type: 'success' | 'error' | 'info';
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
interface Locals {
|
||||
auth: import('lucia').AuthRequest;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,29 @@ export const load: PageServerLoad = async (event) => {
|
|||
});
|
||||
|
||||
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',
|
||||
|
|
@ -48,7 +71,8 @@ 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 = QRCode.toDataURL(totpUri);
|
||||
const qrCode = await QRCode.toDataURL(totpUri);
|
||||
console.log('QR Code: ', qrCode);
|
||||
|
||||
form.data = {
|
||||
current_password: '',
|
||||
|
|
@ -57,6 +81,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
return {
|
||||
form,
|
||||
twoFactorEnabled: false,
|
||||
recoveryCodes: [],
|
||||
totpUri,
|
||||
qrCode,
|
||||
};
|
||||
|
|
@ -129,19 +154,12 @@ export const actions: Actions = {
|
|||
|
||||
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.two_factor_code = '';
|
||||
return { recoveryCodes };
|
||||
return {
|
||||
form,
|
||||
twoFactorEnabled: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,22 +8,49 @@
|
|||
import { addTwoFactorSchema } from '$lib/validations/account';
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
console.log('recovery codes', form.recoveryCodes)
|
||||
const { qrCode, twoFactorEnabled, recoveryCodes } = data;
|
||||
|
||||
const { qrCode } = data;
|
||||
|
||||
const twoFactorForm = superForm(data.form, {
|
||||
const form = superForm(data.form, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(addTwoFactorSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = twoFactorForm;
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<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