mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding show 2FA on login if it is enabled on the user.
This commit is contained in:
parent
826d06113d
commit
8b48466c7e
3 changed files with 47 additions and 16 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { refinePasswords } from "./account";
|
import { refinePasswords } from "./account";
|
||||||
import { userSchema } from "./zod-schemas";
|
import { userSchema } from "./zod-schemas";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
export const signUpSchema = userSchema
|
export const signUpSchema = userSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|
@ -15,7 +16,14 @@ export const signUpSchema = userSchema
|
||||||
refinePasswords(confirm_password, password, ctx);
|
refinePasswords(confirm_password, password, ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const signInSchema = userSchema.pick({
|
export const signInSchema = z.object({
|
||||||
username: true,
|
username: z
|
||||||
password: true
|
.string()
|
||||||
});
|
.trim()
|
||||||
|
.min(3, { message: 'Username must be at least 3 characters' })
|
||||||
|
.max(50, { message: 'Username must be less than 50 characters' }),
|
||||||
|
password: z
|
||||||
|
.string({ required_error: 'Password is required' })
|
||||||
|
.trim(),
|
||||||
|
totpToken: z.string().trim().min(6).max(6).optional()
|
||||||
|
})
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { fail, error, type Actions } from '@sveltejs/kit';
|
import { fail, error, type Actions } from '@sveltejs/kit';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { Argon2id } from 'oslo/password';
|
||||||
|
import { decodeHex } from 'oslo/encoding';
|
||||||
|
import { TOTPController } from 'oslo/otp';
|
||||||
import { zod } from 'sveltekit-superforms/adapters';
|
import { zod } from 'sveltekit-superforms/adapters';
|
||||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
import { redirect } from 'sveltekit-flash-message/server';
|
import { redirect } from 'sveltekit-flash-message/server';
|
||||||
import { Argon2id } from 'oslo/password';
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||||
import db from '$lib/drizzle';
|
import db from '$lib/drizzle';
|
||||||
import { lucia } from '$lib/server/auth';
|
import { lucia } from '$lib/server/auth';
|
||||||
import { signInSchema } from '$lib/validations/auth';
|
import { signInSchema } from '$lib/validations/auth';
|
||||||
import { collections, users, wishlists } from '../../../schema';
|
import { collections, users, wishlists } from '../../../schema';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
|
|
@ -20,13 +22,13 @@ export const load: PageServerLoad = async (event) => {
|
||||||
const form = await superValidate(event, zod(signInSchema));
|
const form = await superValidate(event, zod(signInSchema));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form
|
form,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const limiter = new RateLimiter({
|
const limiter = new RateLimiter({
|
||||||
// A rate is defined by [number, unit]
|
// A rate is defined by [number, unit]
|
||||||
IPUA: [5, 'm']
|
IPUA: [5, 'm'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
|
|
@ -41,7 +43,7 @@ export const actions: Actions = {
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
form.data.password = '';
|
form.data.password = '';
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
form
|
form,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,12 +53,12 @@ export const actions: Actions = {
|
||||||
const password = form.data.password;
|
const password = form.data.password;
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.username, form.data.username)
|
where: eq(users.username, form.data.username),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('user', JSON.stringify(user, null, 2));
|
console.log('user', JSON.stringify(user, null, 2));
|
||||||
|
|
||||||
if (!user || !user.hashed_password) {
|
if (!user?.hashed_password) {
|
||||||
form.data.password = '';
|
form.data.password = '';
|
||||||
return setError(form, '', 'Your username or password is incorrect.');
|
return setError(form, '', 'Your username or password is incorrect.');
|
||||||
}
|
}
|
||||||
|
|
@ -71,21 +73,38 @@ export const actions: Actions = {
|
||||||
await db
|
await db
|
||||||
.insert(collections)
|
.insert(collections)
|
||||||
.values({
|
.values({
|
||||||
user_id: user.id
|
user_id: user.id,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
await db
|
await db
|
||||||
.insert(wishlists)
|
.insert(wishlists)
|
||||||
.values({
|
.values({
|
||||||
user_id: user.id
|
user_id: user.id,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (user?.two_factor_enabled && user?.two_factor_secret && !form?.data?.totpToken) {
|
||||||
|
return setError(
|
||||||
|
form,
|
||||||
|
'totpToken',
|
||||||
|
'Two factor authentication is enabled. Please enter your 2FA code.',
|
||||||
|
);
|
||||||
|
} else if (user?.two_factor_enabled && user?.two_factor_secret && form?.data?.totpToken) {
|
||||||
|
console.log('totpToken', form.data.totpToken);
|
||||||
|
const validOTP = await new TOTPController().verify(
|
||||||
|
form.data.totpToken,
|
||||||
|
decodeHex(user.two_factor_secret)
|
||||||
|
);
|
||||||
|
console.log('validOTP', validOTP);
|
||||||
|
if (!validOTP) {
|
||||||
|
return setError(form, 'totpToken', 'Invalid 2FA code');
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log('ip', locals.ip);
|
console.log('ip', locals.ip);
|
||||||
console.log('country', locals.country);
|
console.log('country', locals.country);
|
||||||
session = await lucia.createSession(user.id, {
|
session = await lucia.createSession(user.id, {
|
||||||
ip_country: locals.country,
|
ip_country: locals.country,
|
||||||
ip_address: locals.ip
|
ip_address: locals.ip,
|
||||||
});
|
});
|
||||||
console.log('logging in session', session);
|
console.log('logging in session', session);
|
||||||
sessionCookie = lucia.createSessionCookie(session.id);
|
sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
|
@ -100,12 +119,12 @@ export const actions: Actions = {
|
||||||
console.log('setting session cookie', sessionCookie);
|
console.log('setting session cookie', sessionCookie);
|
||||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||||
path: '.',
|
path: '.',
|
||||||
...sessionCookie.attributes
|
...sessionCookie.attributes,
|
||||||
});
|
});
|
||||||
|
|
||||||
form.data.username = '';
|
form.data.username = '';
|
||||||
form.data.password = '';
|
form.data.password = '';
|
||||||
const message = { type: 'success', message: 'Signed In!' } as const;
|
const message = { type: 'success', message: 'Signed In!' } as const;
|
||||||
redirect(302, '/', message, event);
|
redirect(302, '/', message, event);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@
|
||||||
<Input type="text" id="username" name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} required />
|
<Input type="text" id="username" name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} required />
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<Input type="password" id="password" name="password" placeholder="Password" autocomplete="password" data-invalid={$errors.password} bind:value={$form.password} required />
|
<Input type="password" id="password" name="password" placeholder="Password" autocomplete="password" data-invalid={$errors.password} bind:value={$form.password} required />
|
||||||
|
{#if $errors.totpToken}
|
||||||
|
<Label for="totpToken">2FA Code</Label>
|
||||||
|
<Input type="text" id="totpToken" name="totpToken" placeholder="2FA Code" autocomplete="one-time-code" data-invalid={$errors.totpToken} bind:value={$form.totpToken} />
|
||||||
|
{/if}
|
||||||
<Button type="submit">Login</Button>
|
<Button type="submit">Login</Button>
|
||||||
{#if $errors._errors}
|
{#if $errors._errors}
|
||||||
<Alert.Root variant="destructive">
|
<Alert.Root variant="destructive">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue