mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding table for two factor codes and utils for tables.
This commit is contained in:
parent
5c3349ca42
commit
d83eaadc0b
17 changed files with 3544 additions and 17 deletions
1
src/db/migrations/0003_premium_ravenous.sql
Normal file
1
src/db/migrations/0003_premium_ravenous.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "users" ADD COLUMN "initiated_time" timestamp;
|
||||
24
src/db/migrations/0004_glossy_gideon.sql
Normal file
24
src/db/migrations/0004_glossy_gideon.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE IF NOT EXISTS "two_factor" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"cuid" text,
|
||||
"two_factor_secret" text NOT NULL,
|
||||
"two_factor_enabled" boolean DEFAULT false NOT NULL,
|
||||
"initiated_time" timestamp with time zone NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "two_factor_cuid_unique" UNIQUE("cuid"),
|
||||
CONSTRAINT "two_factor_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN IF EXISTS "two_factor_secret";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN IF EXISTS "two_factor_enabled";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN IF EXISTS "initiated_time";
|
||||
1650
src/db/migrations/meta/0003_snapshot.json
Normal file
1650
src/db/migrations/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1721
src/db/migrations/meta/0004_snapshot.json
Normal file
1721
src/db/migrations/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,20 @@
|
|||
"when": 1718405257084,
|
||||
"tag": "0002_third_black_tom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1720415770693,
|
||||
"tag": "0003_premium_ravenous",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1720454952583,
|
||||
"tag": "0004_glossy_gideon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -33,3 +33,4 @@ export { default as categories_to_games, categories_to_games_relations } from '.
|
|||
export { default as mechanics, mechanics_relations, type Mechanics } from './mechanics';
|
||||
export { default as mechanicsToExternalIds } from './mechanicsToExternalIds';
|
||||
export { default as mechanics_to_games, mechanics_to_games_relations } from './mechanicsToGames';
|
||||
export { default as twoFactor } from './two-factor.table';
|
||||
|
|
|
|||
35
src/db/schema/two-factor.table.ts
Normal file
35
src/db/schema/two-factor.table.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { timestamps } from '../utils';
|
||||
import users from './users';
|
||||
|
||||
const twoFactorTable = pgTable('two_factor', {
|
||||
id: uuid('id')
|
||||
.primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
two_factor_secret: text('two_factor_secret').notNull(),
|
||||
two_factor_enabled: boolean('two_factor_enabled').notNull().default(false),
|
||||
initiated_time: timestamp('initiated_time', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
}).notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id)
|
||||
.unique(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [twoFactorTable.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type TwoFactor = InferSelectModel<typeof twoFactorTable>;
|
||||
|
||||
export default twoFactorTable;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { timestamps } from '../utils';
|
||||
import user_roles from './userRoles';
|
||||
|
||||
const users = pgTable('users', {
|
||||
|
|
@ -16,10 +17,7 @@ const users = pgTable('users', {
|
|||
verified: boolean('verified').default(false),
|
||||
receive_email: boolean('receive_email').default(false),
|
||||
theme: text('theme').default('system'),
|
||||
two_factor_secret: text('two_factor_secret').default(''),
|
||||
two_factor_enabled: boolean('two_factor_enabled').default(false),
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
updated_at: timestamp('updated_at').notNull().defaultNow(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const user_relations = relations(users, ({ many }) => ({
|
||||
|
|
|
|||
43
src/db/utils.ts
Normal file
43
src/db/utils.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// import { HTTPException } from 'hono/http-exception';
|
||||
import { timestamp } from 'drizzle-orm/pg-core';
|
||||
import { customType } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const citext = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'citext';
|
||||
},
|
||||
});
|
||||
|
||||
export const cuid2 = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'text';
|
||||
},
|
||||
});
|
||||
|
||||
export const takeFirst = <T>(values: T[]): T | null => {
|
||||
if (values.length === 0) return null;
|
||||
return values[0]!;
|
||||
};
|
||||
|
||||
export const takeFirstOrThrow = <T>(values: T[]): T => {
|
||||
if (values.length === 0)
|
||||
// throw new HTTPException(404, {
|
||||
// message: 'Resource not found',
|
||||
// });
|
||||
return values[0]!;
|
||||
};
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: timestamp('created_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp('updated_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
};
|
||||
|
|
@ -25,6 +25,7 @@ const EnvSchema = z.object({
|
|||
DB_SEEDING: stringBoolean,
|
||||
ADMIN_USERNAME: z.string(),
|
||||
ADMIN_PASSWORD: z.string(),
|
||||
TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000),
|
||||
});
|
||||
|
||||
export type EnvSchema = z.infer<typeof EnvSchema>;
|
||||
|
|
|
|||
15
src/lib/components/container.svelte
Normal file
15
src/lib/components/container.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { class: classNames, children, ...restProps }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} class={cn('mx-auto w-full max-w-6xl px-0 md:px-6', classNames)}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
@ -187,11 +187,10 @@ export const actions: Actions = {
|
|||
.where(eq(users.id, user.id));
|
||||
await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id));
|
||||
|
||||
setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
|
||||
return {
|
||||
removeTwoFactorForm,
|
||||
twoFactorEnabled: false,
|
||||
recoveryCodes: [],
|
||||
};
|
||||
// setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
|
||||
redirect(302, '/profile/security/two-factor', {
|
||||
type: 'success',
|
||||
message: 'Two-Factor Authentication has been disabled.',
|
||||
}, event);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import * as Form from '$components/ui/form';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
|
||||
import PinInput from '$components/pin-input.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
<Form.Field form={addTwoFactorForm} name="two_factor_code">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="code">Enter Code</Form.Label>
|
||||
<Input {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
|
||||
<PinInput {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
|
||||
</Form.Control>
|
||||
<Form.Description>This is the code from your authenticator app.</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
|
|
|
|||
|
|
@ -8,11 +8,22 @@
|
|||
{#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}
|
||||
<pre>
|
||||
{#each recoveryCodes as code}
|
||||
<span>{code}</span>
|
||||
{/each}
|
||||
</pre>
|
||||
{/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}
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
pre {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -85,6 +85,12 @@ export const actions: Actions = {
|
|||
|
||||
console.log('ip', locals.ip);
|
||||
console.log('country', locals.country);
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
initiated_time: new Date(),
|
||||
});
|
||||
|
||||
session = await lucia.createSession(user.id, {
|
||||
ip_country: locals.country,
|
||||
ip_address: locals.ip,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { zod } from 'sveltekit-superforms/adapters';
|
|||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||
import { TWO_FACTOR_TIMEOUT } from '../env';
|
||||
import db from '../../../db';
|
||||
import { lucia } from '$lib/server/auth';
|
||||
import { totpSchema } from '$lib/validations/auth';
|
||||
|
|
@ -26,6 +27,12 @@ export const load: PageServerLoad = async (event) => {
|
|||
where: eq(users.username, user.username),
|
||||
});
|
||||
|
||||
// Check if two factor started less than TWO_FACTOR_TIMEOUT
|
||||
if (Date.now() - dbUser?.initiated_time > TWO_FACTOR_TIMEOUT) {
|
||||
const message = { type: 'error', message: 'Two factor authentication has expired' } as const;
|
||||
redirect(302, '/login', message, event);
|
||||
}
|
||||
|
||||
const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated;
|
||||
|
||||
console.log('session', session);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<Input {...attrs} autocomplete="one-time-code" bind:value={$totpForm.totpToken} />
|
||||
{:else}
|
||||
<Form.Label for="totpToken">TOTP Code</Form.Label>
|
||||
<PinInput class="justify-evenly" {...attrs} bind:value={$totpForm.totpToken} />
|
||||
<PinInput {...attrs} bind:value={$totpForm.totpToken} />
|
||||
{/if}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
|
|
|
|||
Loading…
Reference in a new issue