Adding mfa page instead of 2FA, starting controller based password verification and totp generation.

This commit is contained in:
Bradley Shellnut 2024-08-29 16:12:40 -07:00
parent ead20829e4
commit df582f1534
44 changed files with 443 additions and 332 deletions

View file

@ -1,6 +1,6 @@
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) { export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams);
return json({}); return json({});
} }

View file

@ -1,14 +1,14 @@
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import db from '../../../../../db'; import {db} from '$lib/server/api/infrastructure/database';
import { collection_items, usersTable } from '$db/schema'; import { collection_items, usersTable } from '$db/schema';
// Search a user's collection // Search a user's collection
export async function GET({ url, locals, params }) { export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams);
const q = searchParams?.q || ''; const q = searchParams?.q || '';
const limit = parseInt(searchParams?.limit) || 10; const limit = Number.parseInt(searchParams?.limit) || 10;
const skip = parseInt(searchParams?.skip) || 0; const skip = Number.parseInt(searchParams?.skip) || 0;
const order = searchParams?.order || 'asc'; const order = searchParams?.order || 'asc';
const sort = searchParams?.sort || 'name'; const sort = searchParams?.sort || 'name';
const collection_id = params.id; const collection_id = params.id;

View file

@ -1,7 +1,7 @@
import db from '../../../../db'; import { db } from '$lib/server/api/infrastructure/database';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { usersTable } from '$db/schema'; import { usersTable } from '$lib/server/api/infrastructure/database/tables';
import { createPasswordResetToken } from '$lib/server/auth-utils.js'; import { createPasswordResetToken } from '$lib/server/auth-utils.js';
import { PUBLIC_SITE_URL } from '$env/static/public'; import { PUBLIC_SITE_URL } from '$env/static/public';

View file

@ -1,9 +1,8 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { password_reset_tokens, usersTable } from '$db/schema'; import { password_reset_tokens } from '$lib/server/api/infrastructure/database/tables';
import { isWithinExpirationDate } from 'oslo'; import { isWithinExpirationDate } from 'oslo';
import { lucia } from '$lib/server/auth.js'; // import { lucia } from '$lib/server/lucia';
import { Argon2id } from 'oslo/password'; import {db} from '$lib/server/api/infrastructure/database';
import db from '$db';
export async function POST({ request, params }) { export async function POST({ request, params }) {
const { password } = await request.json(); const { password } = await request.json();
@ -32,12 +31,12 @@ export async function POST({ request, params }) {
}); });
} }
await lucia.invalidateUserSessions(token.user_id); // await lucia.invalidateUserSessions(token.user_id);
const hashPassword = await new Argon2id().hash(password); // const hashPassword = await new Argon2id().hash(password);
// await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id)); // // await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
//
const session = await lucia.createSession(token.user_id, {}); // const session = await lucia.createSession(token.user_id, {});
const sessionCookie = lucia.createSessionCookie(session.id); // const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, { return new Response(null, {
status: 302, status: 302,

View file

@ -1,6 +1,6 @@
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) { export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams);
return json({}); return json({});
} }

View file

@ -59,7 +59,7 @@
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6", "svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2", "svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.3", "svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1", "svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
@ -68,7 +68,7 @@
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"tsx": "^4.18.0", "tsx": "^4.19.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.2", "vite": "^5.4.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
@ -106,7 +106,7 @@
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.5.8", "hono": "^4.5.9",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"iconify-icon": "^2.1.0", "iconify-icon": "^2.1.0",

View file

@ -13,13 +13,13 @@ importers:
version: 5.0.14 version: 5.0.14
'@hono/swagger-ui': '@hono/swagger-ui':
specifier: ^0.4.1 specifier: ^0.4.1
version: 0.4.1(hono@4.5.8) version: 0.4.1(hono@4.5.9)
'@hono/zod-openapi': '@hono/zod-openapi':
specifier: ^0.15.3 specifier: ^0.15.3
version: 0.15.3(hono@4.5.8)(zod@3.23.8) version: 0.15.3(hono@4.5.9)(zod@3.23.8)
'@hono/zod-validator': '@hono/zod-validator':
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.2(hono@4.5.8)(zod@3.23.8) version: 0.2.2(hono@4.5.9)(zod@3.23.8)
'@iconify-icons/line-md': '@iconify-icons/line-md':
specifier: ^1.2.30 specifier: ^1.2.30
version: 1.2.30 version: 1.2.30
@ -99,11 +99,11 @@ importers:
specifier: ^4.7.8 specifier: ^4.7.8
version: 4.7.8 version: 4.7.8
hono: hono:
specifier: ^4.5.8 specifier: ^4.5.9
version: 4.5.8 version: 4.5.9
hono-rate-limiter: hono-rate-limiter:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0(hono@4.5.8) version: 0.4.0(hono@4.5.9)
html-entities: html-entities:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
@ -248,7 +248,7 @@ importers:
version: 16.1.0(postcss@8.4.41) version: 16.1.0(postcss@8.4.41)
postcss-load-config: postcss-load-config:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0) version: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
postcss-preset-env: postcss-preset-env:
specifier: ^9.6.0 specifier: ^9.6.0
version: 9.6.0(postcss@8.4.41) version: 9.6.0(postcss@8.4.41)
@ -272,16 +272,16 @@ importers:
version: 5.0.0-next.175 version: 5.0.0-next.175
svelte-check: svelte-check:
specifier: ^3.8.6 specifier: ^3.8.6
version: 3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175) version: 3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)
svelte-headless-table: svelte-headless-table:
specifier: ^0.18.2 specifier: ^0.18.2
version: 0.18.2(svelte@5.0.0-next.175) version: 0.18.2(svelte@5.0.0-next.175)
svelte-meta-tags: svelte-meta-tags:
specifier: ^3.1.3 specifier: ^3.1.4
version: 3.1.3(svelte@5.0.0-next.175)(typescript@5.5.4) version: 3.1.4(svelte@5.0.0-next.175)(typescript@5.5.4)
svelte-preprocess: svelte-preprocess:
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4) version: 6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
svelte-sequential-preprocessor: svelte-sequential-preprocessor:
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
@ -304,8 +304,8 @@ importers:
specifier: ^2.7.0 specifier: ^2.7.0
version: 2.7.0 version: 2.7.0
tsx: tsx:
specifier: ^4.18.0 specifier: ^4.19.0
version: 4.18.0 version: 4.19.0
typescript: typescript:
specifier: ^5.5.4 specifier: ^5.5.4
version: 5.5.4 version: 5.5.4
@ -3178,8 +3178,8 @@ packages:
peerDependencies: peerDependencies:
hono: ^4.1.1 hono: ^4.1.1
hono@4.5.8: hono@4.5.9:
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==} resolution: {integrity: sha512-zz8ktqMDRrZETjxBrv8C5PQRFbrTRCLNVAjD1SNQyOzv4VjmX68Uxw83xQ6oxdAB60HiWnGEatiKA8V3SZLDkQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
html-entities@2.5.2: html-entities@2.5.2:
@ -4493,8 +4493,8 @@ packages:
svelte-lazy-loader@1.0.0: svelte-lazy-loader@1.0.0:
resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==} resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==}
svelte-meta-tags@3.1.3: svelte-meta-tags@3.1.4:
resolution: {integrity: sha512-iIdJgxKdMUqFGR4m88jBE9KTSO2jdKE5CRjyRtAjdevW51jL4TtDZwL7GOtr5Fd2dw/+jyQIPD7APATP191qIA==} resolution: {integrity: sha512-TUIfhut0iVeTm7f5v/ZuU/tZ9XsNig9bNN8yK0t2x2WL9qw6AxAVRe9i5XddYJE0SuVwkoDCzjoSg5hXv7oWbQ==}
peerDependencies: peerDependencies:
svelte: ^3.55.0 || ^4.0.0 svelte: ^3.55.0 || ^4.0.0
@ -4737,8 +4737,8 @@ packages:
tslib@2.7.0: tslib@2.7.0:
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
tsx@4.18.0: tsx@4.19.0:
resolution: {integrity: sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==} resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
@ -5695,20 +5695,20 @@ snapshots:
'@hapi/hoek': 9.3.0 '@hapi/hoek': 9.3.0
optional: true optional: true
'@hono/swagger-ui@0.4.1(hono@4.5.8)': '@hono/swagger-ui@0.4.1(hono@4.5.9)':
dependencies: dependencies:
hono: 4.5.8 hono: 4.5.9
'@hono/zod-openapi@0.15.3(hono@4.5.8)(zod@3.23.8)': '@hono/zod-openapi@0.15.3(hono@4.5.9)(zod@3.23.8)':
dependencies: dependencies:
'@asteasolutions/zod-to-openapi': 7.1.1(zod@3.23.8) '@asteasolutions/zod-to-openapi': 7.1.1(zod@3.23.8)
'@hono/zod-validator': 0.2.2(hono@4.5.8)(zod@3.23.8) '@hono/zod-validator': 0.2.2(hono@4.5.9)(zod@3.23.8)
hono: 4.5.8 hono: 4.5.9
zod: 3.23.8 zod: 3.23.8
'@hono/zod-validator@0.2.2(hono@4.5.8)(zod@3.23.8)': '@hono/zod-validator@0.2.2(hono@4.5.9)(zod@3.23.8)':
dependencies: dependencies:
hono: 4.5.8 hono: 4.5.9
zod: 3.23.8 zod: 3.23.8
'@humanwhocodes/config-array@0.11.14': '@humanwhocodes/config-array@0.11.14':
@ -7562,11 +7562,11 @@ snapshots:
hex-rgb@4.3.0: {} hex-rgb@4.3.0: {}
hono-rate-limiter@0.4.0(hono@4.5.8): hono-rate-limiter@0.4.0(hono@4.5.9):
dependencies: dependencies:
hono: 4.5.8 hono: 4.5.9
hono@4.5.8: {} hono@4.5.9: {}
html-entities@2.5.2: {} html-entities@2.5.2: {}
@ -8301,14 +8301,14 @@ snapshots:
postcss: 8.4.41 postcss: 8.4.41
ts-node: 10.9.2(@types/node@20.16.1)(typescript@5.5.4) ts-node: 10.9.2(@types/node@20.16.1)(typescript@5.5.4)
postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0): postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0):
dependencies: dependencies:
lilconfig: 3.1.1 lilconfig: 3.1.1
yaml: 2.4.2 yaml: 2.4.2
optionalDependencies: optionalDependencies:
jiti: 1.21.6 jiti: 1.21.6
postcss: 8.4.41 postcss: 8.4.41
tsx: 4.18.0 tsx: 4.19.0
postcss-logical@7.0.1(postcss@8.4.41): postcss-logical@7.0.1(postcss@8.4.41):
dependencies: dependencies:
@ -8878,14 +8878,14 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175): svelte-check@3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
chokidar: 3.6.0 chokidar: 3.6.0
picocolors: 1.0.0 picocolors: 1.0.0
sade: 1.8.1 sade: 1.8.1
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4) svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
typescript: 5.5.4 typescript: 5.5.4
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@ -8930,7 +8930,7 @@ snapshots:
svelte-lazy-loader@1.0.0: {} svelte-lazy-loader@1.0.0: {}
svelte-meta-tags@3.1.3(svelte@5.0.0-next.175)(typescript@5.5.4): svelte-meta-tags@3.1.4(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies: dependencies:
schema-dts: 1.1.2(typescript@5.5.4) schema-dts: 1.1.2(typescript@5.5.4)
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
@ -8941,7 +8941,7 @@ snapshots:
dependencies: dependencies:
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4): svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies: dependencies:
'@types/pug': 2.0.10 '@types/pug': 2.0.10
detect-indent: 6.1.0 detect-indent: 6.1.0
@ -8951,16 +8951,16 @@ snapshots:
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
optionalDependencies: optionalDependencies:
postcss: 8.4.41 postcss: 8.4.41
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0) postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
sass: 1.77.8 sass: 1.77.8
typescript: 5.5.4 typescript: 5.5.4
svelte-preprocess@6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4): svelte-preprocess@6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies: dependencies:
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
optionalDependencies: optionalDependencies:
postcss: 8.4.41 postcss: 8.4.41
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0) postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
sass: 1.77.8 sass: 1.77.8
typescript: 5.5.4 typescript: 5.5.4
@ -9174,7 +9174,7 @@ snapshots:
tslib@2.7.0: {} tslib@2.7.0: {}
tsx@4.18.0: tsx@4.19.0:
dependencies: dependencies:
esbuild: 0.23.0 esbuild: 0.23.0
get-tsconfig: 4.7.5 get-tsconfig: 4.7.5

View file

@ -92,7 +92,7 @@
</DropdownMenu.Root> </DropdownMenu.Root>
{:else} {:else}
<a href="/login"> <span class="flex-auto">Login</span></a> <a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/sign-up"> <span class="flex-auto">Sign Up</span></a> <a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if} {/if}
</nav> </nav>
</header> </header>

View file

@ -13,7 +13,7 @@
// $: termsValue = $form.terms as Writable<boolean>; // $: termsValue = $form.terms as Writable<boolean>;
</script> </script>
<form method="POST" action="/sign-up" use:enhance> <form method="POST" action="/signup" use:enhance>
<h1>Signup user</h1> <h1>Signup user</h1>
<label class="label"> <label class="label">
<span class="sr-only">First Name</span> <span class="sr-only">First Name</span>

View file

@ -0,0 +1,7 @@
import { z } from 'zod'
export const verifyPasswordDto = z.object({
password: z.string({ required_error: 'Password is required' }).trim(),
})
export type VerifyPasswordDto = z.infer<typeof verifyPasswordDto>

View file

@ -11,6 +11,7 @@ import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { updateProfileDto } from '$lib/dtos/update-profile.dto' import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { updateEmailDto } from '$lib/dtos/update-email.dto' import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes'
import { verifyPasswordDto } from '$lib/dtos/verify-password.dto'
@injectable() @injectable()
export class IamController implements Controller { export class IamController implements Controller {
@ -36,6 +37,15 @@ export class IamController implements Controller {
} }
return c.json({ user: updatedUser }, StatusCodes.OK) return c.json({ user: updatedUser }, StatusCodes.OK)
}) })
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user
const { password } = c.req.valid('json')
const passwordVerified = await this.iamService.verifyPassword(user.id, { password })
if (!passwordVerified) {
return c.json('Incorrect password', StatusCodes.BAD_REQUEST)
}
return c.json({ }, StatusCodes.OK)
})
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user const user = c.var.user
const { email } = c.req.valid('json') const { email } = c.req.valid('json')

View file

@ -0,0 +1,39 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types';
import type { Controller } from '$lib/server/api/interfaces/controller.interface';
import { TotpService } from '$lib/server/api/services/totp.service';
import {StatusCodes} from "$lib/constants/status-codes";
@injectable()
export class MfaController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject(TotpService) private readonly totpService: TotpService
) {
}
routes() {
return this.controller
.get('/totp', requireAuth, async (c) => {
const user = c.var.user;
const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential });
})
.post('/totp', requireAuth, async (c) => {
const user = c.var.user;
const totpCredential = await this.totpService.create(user.id);
return c.json({ totpCredential })
})
.delete('/totp', requireAuth, async (c) => {
const user = c.var.user;
await this.totpService.deleteOneByUserId(user.id);
return c.status(StatusCodes.NO_CONTENT);
});
}
}

View file

@ -8,6 +8,7 @@ import { config } from './common/config';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
import { IamController } from './controllers/iam.controller'; import { IamController } from './controllers/iam.controller';
import { LoginController } from './controllers/login.controller'; import { LoginController } from './controllers/login.controller';
import { MfaController} from "$lib/server/api/controllers/mfa.controller";
import {UserController} from "$lib/server/api/controllers/user.controller"; import {UserController} from "$lib/server/api/controllers/user.controller";
import {SignupController} from "$lib/server/api/controllers/signup.controller"; import {SignupController} from "$lib/server/api/controllers/signup.controller";
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller"; import {WishlistController} from "$lib/server/api/controllers/wishlist.controller";
@ -44,6 +45,7 @@ const routes = app
.route('/signup', container.resolve(SignupController).routes()) .route('/signup', container.resolve(SignupController).routes())
.route('/wishlists', container.resolve(WishlistController).routes()) .route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes()) .route('/collections', container.resolve(CollectionController).routes())
.route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' })); .get('/', (c) => c.json({ message: 'Server is healthy' }));
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */

View file

@ -31,7 +31,7 @@ for (const table of [
schema.publishers, schema.publishers,
schema.publishersToExternalIds, schema.publishersToExternalIds,
schema.publishers_to_games, schema.publishers_to_games,
schema.recovery_codes, schema.recoveryCodesTable,
schema.roles, schema.roles,
schema.sessionsTable, schema.sessionsTable,
schema.twoFactorTable, schema.twoFactorTable,

View file

@ -16,7 +16,7 @@ export * from './passwordResetTokens';
export * from './publishers'; export * from './publishers';
export * from './publishersToExternalIds'; export * from './publishersToExternalIds';
export * from './publishersToGames'; export * from './publishersToGames';
export * from './recoveryCodes'; export * from './recovery-codes.table';
export * from './roles'; export * from './roles';
export * from './sessions.table'; export * from './sessions.table';
export * from './two-factor.table'; export * from './two-factor.table';

View file

@ -3,7 +3,7 @@ import type { InferSelectModel } from 'drizzle-orm';
import { usersTable } from './users.table'; import { usersTable } from './users.table';
import { timestamps } from '../utils'; import { timestamps } from '../utils';
export const recovery_codes = pgTable('recovery_codes', { export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id') userId: uuid('user_id')
.notNull() .notNull()
@ -13,4 +13,4 @@ export const recovery_codes = pgTable('recovery_codes', {
...timestamps, ...timestamps,
}); });
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>; export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>;

View file

@ -17,6 +17,15 @@ export class CredentialsRepository {
}); });
} }
async findOneByUserIdAndType(userId: string, type: CredentialsType) {
return this.db.query.credentialsTable.findFirst({
where: and(
eq(credentialsTable.user_id, userId),
eq(credentialsTable.type, type)
)
});
}
async findPasswordCredentialsByUserId(userId: string) { async findPasswordCredentialsByUserId(userId: string) {
return this.db.query.credentialsTable.findFirst({ return this.db.query.credentialsTable.findFirst({
where: and( where: and(
@ -59,4 +68,16 @@ export class CredentialsRepository {
.returning() .returning()
.then(takeFirstOrThrow); .then(takeFirstOrThrow);
} }
async delete(id: string) {
return this.db
.delete(credentialsTable)
.where(eq(credentialsTable.id, id));
}
async deleteByUserId(userId: string) {
return this.db
.delete(credentialsTable)
.where(eq(credentialsTable.user_id, userId));
}
} }

View file

@ -66,4 +66,13 @@ export class IamService {
email, email,
}); });
} }
async verifyPassword(userId: string, data: VerifyPasswordDto) {
const user = await this.usersService.findOneById(userId);
if (!user) {
return null;
}
const { password } = data;
return this.usersService.verifyPassword(userId, { password });
}
} }

View file

@ -0,0 +1,36 @@
import { inject, injectable } from "tsyringe";
import { HMAC } from 'oslo/crypto';
import { encodeHex } from 'oslo/encoding';
import {CredentialsRepository} from "$lib/server/api/repositories/credentials.repository";
@injectable()
export class TotpService {
constructor(
@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository
) {
}
async findOneByUserId(userId: string) {
return this.credentialsRepository.findTOTPCredentialsByUserId(userId);
}
async create(userId: string) {
const twoFactorSecret = await new HMAC('SHA-1').generateKey();
try {
return await this.credentialsRepository.create({
user_id: userId,
secret_data: encodeHex(twoFactorSecret),
type: 'totp'
});
} catch (e) {
console.error(e);
return null;
}
}
async deleteOneByUserId(userId: string) {
return this.credentialsRepository.deleteByUserId(userId);
}
}

View file

@ -68,4 +68,17 @@ export class UsersService {
async findOneById(id: string) { async findOneById(id: string) {
return this.usersRepository.findOneById(id); return this.usersRepository.findOneById(id);
} }
async verifyPassword(userId: string, data: { password: string }) {
const user = await this.usersRepository.findOneById(userId);
if (!user) {
throw new Error('User not found');
}
const credential = await this.credentialsRepository.findOneByUserIdAndType(userId, CredentialsType.PASSWORD);
if (!credential) {
throw new Error('Password credentials not found');
}
const { password } = data;
return this.tokenService.verifyHashedToken(credential.secret_data, password);
}
} }

View file

@ -1,20 +1,20 @@
import { redirect } from 'sveltekit-flash-message/server'; import { redirect } from 'sveltekit-flash-message/server';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import db from '../../../../db'; import { db } from '$lib/server/api/infrastructure/database';
import { wishlists } from '$db/schema'; import { wishlists } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
export async function load(event) { export async function load(event) {
const { locals } = event; const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
try { try {
const dbWishlists = await db.query.wishlists.findMany({ const dbWishlists = await db.query.wishlists.findMany({
where: eq(wishlists.user_id, user!.id!), where: eq(wishlists.user_id, authedUser.id),
}); });
return { return {

View file

@ -10,129 +10,129 @@ import { db } from '$lib/server/api/infrastructure/database';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { usersTable, credentialsTable } from '$lib/server/api/infrastructure/database/tables'; import { usersTable, credentialsTable } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { userNotAuthenticated } from '$lib/server/auth-utils';
import {updateProfileDto} from "$lib/dtos/update-profile.dto"; import { updateProfileDto } from "$lib/dtos/update-profile.dto";
import {updateEmailDto} from "$lib/dtos/update-email.dto"; import { updateEmailDto } from "$lib/dtos/update-email.dto";
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
console.log('authedUser', authedUser); console.log('authedUser', authedUser);
// if (userNotAuthenticated(user, session)) { // if (userNotAuthenticated(user, session)) {
// redirect(302, '/login', notSignedInMessage, event); // redirect(302, '/login', notSignedInMessage, event);
// } // }
// const dbUser = await db.query.usersTable.findFirst({ // const dbUser = await db.query.usersTable.findFirst({
// where: eq(usersTable.id, user!.id!), // where: eq(usersTable.id, user!.id!),
// }); // });
const profileForm = await superValidate(zod(profileSchema), { const profileForm = await superValidate(zod(profileSchema), {
defaults: { defaults: {
firstName: authedUser?.firstName ?? '', firstName: authedUser?.firstName ?? '',
lastName: authedUser?.lastName ?? '', lastName: authedUser?.lastName ?? '',
username: authedUser?.username ?? '', username: authedUser?.username ?? '',
}, },
}); });
const emailForm = await superValidate(zod(changeEmailSchema), { const emailForm = await superValidate(zod(changeEmailSchema), {
defaults: { defaults: {
email: authedUser?.email ?? '', email: authedUser?.email ?? '',
}, },
}); });
// const twoFactorDetails = await db.query.twoFactor.findFirst({ // const twoFactorDetails = await db.query.twoFactor.findFirst({
// where: eq(twoFactor.userId, authedUser!.id!), // where: eq(twoFactor.userId, authedUser!.id!),
// }); // });
return { return {
profileForm, profileForm,
emailForm, emailForm,
hasSetupTwoFactor: false //!!twoFactorDetails?.enabled, hasSetupTwoFactor: false //!!twoFactorDetails?.enabled,
}; };
}; };
const changeEmailIfNotEmpty = z.object({ const changeEmailIfNotEmpty = z.object({
email: z email: z
.string() .string()
.trim() .trim()
.max(64, { message: 'Email must be less than 64 characters' }) .max(64, { message: 'Email must be less than 64 characters' })
.email({ message: 'Please enter a valid email' }), .email({ message: 'Please enter a valid email' }),
}); });
export const actions: Actions = { export const actions: Actions = {
profileUpdate: async (event) => { profileUpdate: async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
redirect(302, '/login', notSignedInMessage, event); redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate(event, zod(updateProfileDto)); const form = await superValidate(event, zod(updateProfileDto));
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parwseApiResponse); const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse);
console.log('data from profile update', error); console.log('data from profile update', error);
if (error) { if (error) {
return setError(form, 'username', error); return setError(form, 'username', error);
} }
if (!form.valid) { if (!form.valid) {
return fail(400, { return fail(400, {
form, form,
}); });
} }
console.log('profile updated successfully'); console.log('profile updated successfully');
return message(form, { type: 'success', message: 'Profile updated successfully!' }); return message(form, { type: 'success', message: 'Profile updated successfully!' });
}, },
changeEmail: async (event) => { changeEmail: async (event) => {
const form = await superValidate(event, zod(updateEmailDto)); const form = await superValidate(event, zod(updateEmailDto));
const newEmail = form.data?.email; const newEmail = form.data?.email;
if ( if (
!form.valid || !form.valid ||
!newEmail || !newEmail ||
(newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success) (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)
) { ) {
return fail(400, { return fail(400, {
form, form,
}); });
} }
if (!event.locals.user) { if (!event.locals.user) {
redirect(302, '/login', notSignedInMessage, event); redirect(302, '/login', notSignedInMessage, event);
} }
const user = event.locals.user; const user = event.locals.user;
const existingUser = await db.query.usersTable.findFirst({ const existingUser = await db.query.usersTable.findFirst({
where: eq(usersTable.email, newEmail), where: eq(usersTable.email, newEmail),
}); });
if (existingUser && existingUser.id !== user.id) { if (existingUser && existingUser.id !== user.id) {
return setError(form, 'email', 'That email is already taken'); return setError(form, 'email', 'That email is already taken');
} }
await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id)); await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id));
// if (user.email !== form.data.email) { // if (user.email !== form.data.email) {
// Send email to confirm new email? // Send email to confirm new email?
// auth.update // auth.update
// await locals.prisma.key.update({ // await locals.prisma.key.update({
// where: { // where: {
// id: 'emailpassword:' + user.email // id: 'emailpassword:' + user.email
// }, // },
// data: { // data: {
// id: 'emailpassword:' + form.data.email // id: 'emailpassword:' + form.data.email
// } // }
// }); // });
// auth.updateUserAttributes(user.user_id, { // auth.updateUserAttributes(user.user_id, {
// receiveEmail: false // receiveEmail: false
// }); // });
// } // }
return message(form, { type: 'success', message: 'Email updated successfully!' }); return message(form, { type: 'success', message: 'Email updated successfully!' });
}, },
}; };

View file

@ -96,16 +96,16 @@
</form> </form>
<div class="mt-6"> <div class="mt-6">
{#if !hasSetupTwoFactor} {#if !hasSetupTwoFactor}
<p>Two Factor Authentication is: <strong>Disabled</strong></p> <p>Multi Factor Authentication is: <strong>Disabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor"> <Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
<KeyRound class="mr-2 h-4 w-4" /> <KeyRound class="mr-2 h-4 w-4" />
Setup 2FA Setup Multi-factor Authentication
</Button> </Button>
{:else} {:else}
<p>Two Factor Authentication is: <strong>Enabled</strong></p> <p>Multi Factor Authentication is: <strong>Enabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor"> <Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
<KeyRound class="mr-2 h-4 w-4" /> <KeyRound class="mr-2 h-4 w-4" />
Disable 2FA Disable Multi-factor Authentication
</Button> </Button>
{/if} {/if}
</div> </div>

View file

@ -12,30 +12,32 @@ import { redirect, setFlash } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../$types'; import type { PageServerLoad } from '../../$types';
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account'; import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
import db from '$lib/server/api/infrastructure/database'; import { db } from '$lib/server/api/infrastructure/database';
import { recoveryCodes, twoFactor, usersTable } from '$lib/server/api/infrastructure/database/tables'; import { recoveryCodesTable, credentialsTable, usersTable, type Credentials } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { userNotAuthenticated } from '$lib/server/auth-utils';
import env from '../../../../../../env'; import env from '$src/env';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
const { locals } = event; const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
const dbUser = await db.query.usersTable.findFirst({ const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
where: eq(usersTable.id, user!.id!), const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
}); // const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
const twoFactorDetails = await db.query.twoFactor.findFirst({ const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
where: eq(twoFactor.userId, dbUser!.id!), if (error || !data) {
}); return fail(500, {
addTwoFactorForm,
});
}
const { totpCredential } = data
if (twoFactorDetails?.enabled) { if (totpCredential) {
return { return {
addTwoFactorForm, addTwoFactorForm,
removeTwoFactorForm, removeTwoFactorForm,
@ -43,35 +45,27 @@ export const load: PageServerLoad = async (event) => {
recoveryCodes: [], recoveryCodes: [],
totpUri: '', totpUri: '',
qrCode: '', qrCode: '',
}; }
}
const twoFactorSecret = await new HMAC('SHA-1').generateKey();
try {
await db
.insert(twoFactor)
.values({
secret: encodeHex(twoFactorSecret),
enabled: false,
userId: dbUser!.id!,
})
.onConflictDoUpdate({
target: twoFactor.userId,
set: {
secret: encodeHex(twoFactorSecret),
enabled: false,
},
});
} catch (e) {
console.error(e);
error(500);
} }
const issuer = kebabCase(env.PUBLIC_SITE_NAME); const issuer = kebabCase(env.PUBLIC_SITE_NAME);
const accountName = dbUser!.email! || dbUser!.username!; const accountName = authedUser.email || authedUser.username;
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse);
if (createdTotpError || !createdTotpData) {
return fail(500, {
addTwoFactorForm,
})
}
const { totpCredential: createdTotpCredentials } = createdTotpData;
// 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); if (!createdTotpCredentials?.secret_data) {
return fail(500, {
addTwoFactorForm,
})
}
const totpUri = createTOTPKeyURI(issuer, accountName, createdTotpCredentials.secret_data);
addTwoFactorForm.data = { addTwoFactorForm.data = {
current_password: '', current_password: '',
@ -88,111 +82,85 @@ export const load: PageServerLoad = async (event) => {
}; };
export const actions: Actions = { export const actions: Actions = {
enableTwoFactor: async (event) => { enableTotp: async (event) => {
const { locals } = event; const { locals } = event
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema)); const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema))
if (!addTwoFactorForm.valid) { if (!addTwoFactorForm.valid) {
return fail(400, { return fail(400, {
addTwoFactorForm, addTwoFactorForm,
}); })
} }
if (!event.locals.user) { const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
redirect(302, '/login', notSignedInMessage, event); if (error || !data) {
} return fail(500, {
if (!event.locals.session) {
return fail(401);
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id!),
});
// if (!dbUser?.hashed_password) {
// addTwoFactorForm.data.current_password = '';
// addTwoFactorForm.data.two_factor_code = '';
// return setError(
// addTwoFactorForm,
// 'Error occurred. Please try again or contact support if you need further help.',
// );
// }
const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser?.id),
});
if (!twoFactorDetails) {
addTwoFactorForm.data.current_password = '';
addTwoFactorForm.data.two_factor_code = '';
return setError(
addTwoFactorForm, addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.', })
); }
const { totpCredential } = data
if (!totpCredential) {
addTwoFactorForm.data.current_password = ''
addTwoFactorForm.data.two_factor_code = ''
return setError(addTwoFactorForm, 'Error occurred. Please try again or contact support if you need further help.')
} }
if (twoFactorDetails.secret === '' || twoFactorDetails.secret === null) { if (totpCredential.secret_data === '' || totpCredential.secret_data === null) {
addTwoFactorForm.data.current_password = ''; addTwoFactorForm.data.current_password = ''
addTwoFactorForm.data.two_factor_code = ''; addTwoFactorForm.data.two_factor_code = ''
return setError( return setError(addTwoFactorForm, 'Error occurred. Please try again or contact support if you need further help.')
addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
} }
const currentPasswordVerified = await new Argon2id().verify( const currentPasswordVerified = await locals.api.me.verify.password.$post({
// dbUser.hashed_password, json: { password: addTwoFactorForm.data.current_password },
addTwoFactorForm.data.current_password, });
);
if (!currentPasswordVerified) { if (!currentPasswordVerified) {
return setError(addTwoFactorForm, 'current_password', 'Your password is incorrect'); return setError(addTwoFactorForm, 'current_password', 'Your password is incorrect')
} }
if (addTwoFactorForm.data.two_factor_code === '') { if (addTwoFactorForm.data.two_factor_code === '') {
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code'); return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code')
} }
const twoFactorCode = addTwoFactorForm.data.two_factor_code; const twoFactorCode = addTwoFactorForm.data.two_factor_code
const validOTP = await new TOTPController().verify( const validOTP = await new TOTPController().verify(twoFactorCode, decodeHex(twoFactorDetails.secret))
twoFactorCode,
decodeHex(twoFactorDetails.secret),
);
if (!validOTP) { if (!validOTP) {
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code'); return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code')
} }
await db.update(twoFactor).set({ enabled: true }).where(eq(twoFactor.userId, user!.id!)); await db.update(twoFactor).set({ enabled: true }).where(eq(twoFactor.userId, user!.id!))
redirect(302, '/profile/security/two-factor/recovery-codes'); redirect(302, '/profile/security/two-factor/recovery-codes')
}, },
disableTwoFactor: async (event) => { disableTotp: async (event) => {
const { locals } = event; const { locals } = event
const { user, session } = locals; const { user, session } = locals
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema)); const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema))
if (!removeTwoFactorForm.valid) { if (!removeTwoFactorForm.valid) {
return fail(400, { return fail(400, {
removeTwoFactorForm, removeTwoFactorForm,
}); })
} }
if (!user || !session) { if (!user || !session) {
return fail(401, { return fail(401, {
removeTwoFactorForm, removeTwoFactorForm,
}); })
} }
const dbUser = await db.query.usersTable.findFirst({ const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user.id), where: eq(usersTable.id, user.id),
}); })
// if (!dbUser?.hashed_password) { // if (!dbUser?.hashed_password) {
// removeTwoFactorForm.data.current_password = ''; // removeTwoFactorForm.data.current_password = '';
@ -205,24 +173,24 @@ export const actions: Actions = {
const currentPasswordVerified = await new Argon2id().verify( const currentPasswordVerified = await new Argon2id().verify(
// dbUser.hashed_password, // dbUser.hashed_password,
removeTwoFactorForm.data.current_password, removeTwoFactorForm.data.current_password,
); )
if (!currentPasswordVerified) { if (!currentPasswordVerified) {
return setError(removeTwoFactorForm, 'current_password', 'Your password is incorrect'); return setError(removeTwoFactorForm, 'current_password', 'Your password is incorrect')
} }
const twoFactorDetails = await db.query.twoFactor.findFirst({ const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser.id), where: eq(twoFactor.userId, dbUser.id),
}); })
if (!twoFactorDetails) { if (!twoFactorDetails) {
return fail(500, { return fail(500, {
removeTwoFactorForm, removeTwoFactorForm,
}); })
} }
await db.update(twoFactor).set({ enabled: false }).where(eq(twoFactor.userId, user.id)); await db.update(twoFactor).set({ enabled: false }).where(eq(twoFactor.userId, user.id))
await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id)); await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id))
// setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies); // setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
redirect( redirect(
@ -233,6 +201,6 @@ export const actions: Actions = {
message: 'Two-Factor Authentication has been disabled.', message: 'Two-Factor Authentication has been disabled.',
}, },
event, event,
); )
}, },
}; }

View file

@ -37,7 +37,7 @@
{#if twoFactorEnabled} {#if twoFactorEnabled}
<h2>Currently you have two factor authentication <span class="text-green-500">enabled</span></h2> <h2>Currently you have two factor authentication <span class="text-green-500">enabled</span></h2>
<p>To disable two factor authentication, please enter your current password.</p> <p>To disable two factor authentication, please enter your current password.</p>
<form method="POST" action="?/disableTwoFactor" use:removeTwoFactorEnhance data-sveltekit-replacestate> <form method="POST" action="?/disableTotp" use:removeTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={removeTwoFactorForm} name="current_password"> <Form.Field form={removeTwoFactorForm} name="current_password">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label for="password">Current Password</Form.Label> <Form.Label for="password">Current Password</Form.Label>
@ -51,7 +51,7 @@
{:else} {:else}
<h2>Please scan the following QR Code</h2> <h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" /> <img src={qrCode} alt="QR Code" />
<form method="POST" action="?/enableTwoFactor" use:addTwoFactorEnhance data-sveltekit-replacestate> <form method="POST" action="?/enableTotp" use:addTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={addTwoFactorForm} name="two_factor_code"> <Form.Field form={addTwoFactorForm} name="two_factor_code">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label for="code">Enter Code</Form.Label> <Form.Label for="code">Enter Code</Form.Label>

View file

@ -1,31 +1,36 @@
import db from '../../../../../../../db';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password'; import { Argon2id } from 'oslo/password';
import { alphabet, generateRandomString } from 'oslo/crypto'; import { alphabet, generateRandomString } from 'oslo/crypto';
import { redirect } from 'sveltekit-flash-message/server'; import { redirect } from 'sveltekit-flash-message/server';
import { db } from '$lib/server/api/infrastructure/database';
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
import type { PageServerLoad } from '../../../$types'; import type { PageServerLoad } from '../../../$types';
import {recoveryCodes, twoFactor, usersTable} from '$db/schema'; import { recoveryCodesTable, twoFactorTable, usersTable} from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { userNotAuthenticated } from '$lib/server/auth-utils';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
const dbUser = await db.query.usersTable.findFirst({ const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id), where: eq(usersTable.id, authedUser.id),
}); });
if (!dbUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const twoFactorDetails = await db.query.twoFactor.findFirst({ const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser!.id), where: eq(twoFactor.userId, dbUser.id),
}); });
if (twoFactorDetails?.enabled) { if (twoFactorDetails?.enabled) {
const dbRecoveryCodes = await db.query.recoveryCodes.findMany({ const dbRecoveryCodes = await db.query.recoveryCodes.findMany({
where: eq(recoveryCodes.userId, user!.id), where: eq(recoveryCodes.userId, authedUser.id),
}); });
if (dbRecoveryCodes.length === 0) { if (dbRecoveryCodes.length === 0) {
@ -37,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
const hashedCode = await new Argon2id().hash(code); const hashedCode = await new Argon2id().hash(code);
console.log('Inserting recovery code', code, hashedCode); console.log('Inserting recovery code', code, hashedCode);
await db.insert(recoveryCodes).values({ await db.insert(recoveryCodes).values({
userId: user!.id, userId: authedUser.id,
code: hashedCode, code: hashedCode,
}); });
} }

View file

@ -5,22 +5,22 @@ 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 { Argon2id } from 'oslo/password';
import type { PageServerLoad } from '../../../$types'; import type { PageServerLoad } from '../../../$types';
import db from '../../../../../../../db'; import { db } from '$lib/server/api/infrastructure/database';
import { changeUserPasswordSchema } from '$lib/validations/account'; import { changeUserPasswordSchema } from '$lib/validations/account';
import { lucia } from '$lib/server/auth.js'; import { usersTable } from '$lib/server/api/infrastructure/database/tables';
import { usersTable } from '$db/schema';
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
import type { Cookie } from 'lucia'; import type { Cookie } from 'lucia';
import { userNotAuthenticated } from '$lib/server/auth-utils';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const form = await superValidate(event, zod(changeUserPasswordSchema));
const { locals } = event; const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate(event, zod(changeUserPasswordSchema));
form.data = { form.data = {
current_password: '', current_password: '',
password: '', password: '',
@ -34,9 +34,10 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {
const { locals } = event; const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
return fail(401); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate(event, zod(changeUserPasswordSchema)); const form = await superValidate(event, zod(changeUserPasswordSchema));
@ -57,7 +58,7 @@ export const actions: Actions = {
} }
const dbUser = await db.query.usersTable.findFirst({ const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id), where: eq(usersTable.id, authedUser.id),
}); });
// if (!dbUser?.hashed_password) { // if (!dbUser?.hashed_password) {
@ -78,14 +79,14 @@ export const actions: Actions = {
if (!currentPasswordVerified) { if (!currentPasswordVerified) {
return setError(form, 'current_password', 'Your password is incorrect'); return setError(form, 'current_password', 'Your password is incorrect');
} }
if (user?.username) { if (authedUser?.username) {
let sessionCookie: Cookie; let sessionCookie: Cookie;
try { try {
if (form.data.password !== form.data.confirm_password) { if (form.data.password !== form.data.confirm_password) {
return setError(form, 'Password and confirm password do not match'); return setError(form, 'Password and confirm password do not match');
} }
const hashedPassword = await new Argon2id().hash(form.data.password); const hashedPassword = await new Argon2id().hash(form.data.password);
await lucia.invalidateUserSessions(user.id); await lucia.invalidateUserSessions(authedUser.id);
// await db // await db
// .update(usersTable) // .update(usersTable)
// .set({ hashed_password: hashedPassword }) // .set({ hashed_password: hashedPassword })

View file

@ -52,7 +52,7 @@
<AddToList {in_collection} {in_wishlist} game_id={game.id} {wishlist} {collection} /> <AddToList {in_collection} {in_wishlist} game_id={game.id} {wishlist} {collection} />
{:else} {:else}
<span> <span>
<Button href="/sign-up">Sign Up</Button> or <Button href="/login">Sign In</Button> to add to a list. <Button href="/signup">Sign Up</Button> or <Button href="/login">Sign In</Button> to add to a list.
</span> </span>
{/if} {/if}
</div> </div>

View file

@ -18,8 +18,8 @@
{#if $page.url.pathname !== '/login'} {#if $page.url.pathname !== '/login'}
<Button href="/login" variant="ghost">Login</Button> <Button href="/login" variant="ghost">Login</Button>
{/if} {/if}
{#if $page.url.pathname !== '/sign-up'} {#if $page.url.pathname !== '/signup'}
<Button href="/sign-up" variant="ghost">Sign up</Button> <Button href="/signup" variant="ghost">Sign up</Button>
{/if} {/if}
</div> </div>
<div class="auth-marketing"> <div class="auth-marketing">

View file

@ -46,7 +46,7 @@
<Card.Title class="text-2xl">Signup for an account</Card.Title> <Card.Title class="text-2xl">Signup for an account</Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<form method="POST" action="/sign-up" use:enhance class="grid gap-2 mt-4"> <form method="POST" action="/signup" use:enhance class="grid gap-2 mt-4">
<Label for="username">Username <small>(required)</small></Label> <Label for="username">Username <small>(required)</small></Label>
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username" <Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username"
placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} /> placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />

View file

@ -22,9 +22,10 @@ const config = {
$assets: './src/assets', $assets: './src/assets',
$components: './src/components', $components: './src/components',
'$components/*': 'src/lib/components/*', '$components/*': 'src/lib/components/*',
$db: './src/db', $db: './src/lib/server/api/infrastructure/database',
$server: './src/server', $server: './src/server',
$lib: './src/lib', $lib: './src/lib',
$src: './src',
$state: './src/state', $state: './src/state',
$styles: './src/styles', $styles: './src/styles',
$themes: './src/themes', $themes: './src/themes',