mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding mfa page instead of 2FA, starting controller based password verification and totp generation.
This commit is contained in:
parent
ead20829e4
commit
df582f1534
44 changed files with 443 additions and 332 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import db from '../../../../../db';
|
||||
import {db} from '$lib/server/api/infrastructure/database';
|
||||
import { collection_items, usersTable } from '$db/schema';
|
||||
|
||||
// Search a user's collection
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
const q = searchParams?.q || '';
|
||||
const limit = parseInt(searchParams?.limit) || 10;
|
||||
const skip = parseInt(searchParams?.skip) || 0;
|
||||
const limit = Number.parseInt(searchParams?.limit) || 10;
|
||||
const skip = Number.parseInt(searchParams?.skip) || 0;
|
||||
const order = searchParams?.order || 'asc';
|
||||
const sort = searchParams?.sort || 'name';
|
||||
const collection_id = params.id;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import db from '../../../../db';
|
||||
import { db } from '$lib/server/api/infrastructure/database';
|
||||
import { error } from '@sveltejs/kit';
|
||||
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 { PUBLIC_SITE_URL } from '$env/static/public';
|
||||
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
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 { lucia } from '$lib/server/auth.js';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import db from '$db';
|
||||
// import { lucia } from '$lib/server/lucia';
|
||||
import {db} from '$lib/server/api/infrastructure/database';
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const { password } = await request.json();
|
||||
|
|
@ -32,12 +31,12 @@ export async function POST({ request, params }) {
|
|||
});
|
||||
}
|
||||
|
||||
await lucia.invalidateUserSessions(token.user_id);
|
||||
const hashPassword = await new Argon2id().hash(password);
|
||||
// await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
|
||||
|
||||
const session = await lucia.createSession(token.user_id, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
// await lucia.invalidateUserSessions(token.user_id);
|
||||
// const hashPassword = await new Argon2id().hash(password);
|
||||
// // await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
|
||||
//
|
||||
// const session = await lucia.createSession(token.user_id, {});
|
||||
// const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"svelte": "5.0.0-next.175",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-headless-table": "^0.18.2",
|
||||
"svelte-meta-tags": "^3.1.3",
|
||||
"svelte-meta-tags": "^3.1.4",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"sveltekit-flash-message": "^2.4.4",
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"tailwindcss": "^3.4.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.7.0",
|
||||
"tsx": "^4.18.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.2",
|
||||
"vitest": "^1.6.0",
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
"feather-icons": "^4.29.2",
|
||||
"formsnap": "^1.0.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"hono": "^4.5.8",
|
||||
"hono": "^4.5.9",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.1.0",
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ importers:
|
|||
version: 5.0.14
|
||||
'@hono/swagger-ui':
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(hono@4.5.8)
|
||||
version: 0.4.1(hono@4.5.9)
|
||||
'@hono/zod-openapi':
|
||||
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':
|
||||
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':
|
||||
specifier: ^1.2.30
|
||||
version: 1.2.30
|
||||
|
|
@ -99,11 +99,11 @@ importers:
|
|||
specifier: ^4.7.8
|
||||
version: 4.7.8
|
||||
hono:
|
||||
specifier: ^4.5.8
|
||||
version: 4.5.8
|
||||
specifier: ^4.5.9
|
||||
version: 4.5.9
|
||||
hono-rate-limiter:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(hono@4.5.8)
|
||||
version: 0.4.0(hono@4.5.9)
|
||||
html-entities:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
|
|
@ -248,7 +248,7 @@ importers:
|
|||
version: 16.1.0(postcss@8.4.41)
|
||||
postcss-load-config:
|
||||
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:
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0(postcss@8.4.41)
|
||||
|
|
@ -272,16 +272,16 @@ importers:
|
|||
version: 5.0.0-next.175
|
||||
svelte-check:
|
||||
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:
|
||||
specifier: ^0.18.2
|
||||
version: 0.18.2(svelte@5.0.0-next.175)
|
||||
svelte-meta-tags:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3(svelte@5.0.0-next.175)(typescript@5.5.4)
|
||||
specifier: ^3.1.4
|
||||
version: 3.1.4(svelte@5.0.0-next.175)(typescript@5.5.4)
|
||||
svelte-preprocess:
|
||||
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:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
|
|
@ -304,8 +304,8 @@ importers:
|
|||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
tsx:
|
||||
specifier: ^4.18.0
|
||||
version: 4.18.0
|
||||
specifier: ^4.19.0
|
||||
version: 4.19.0
|
||||
typescript:
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
|
|
@ -3178,8 +3178,8 @@ packages:
|
|||
peerDependencies:
|
||||
hono: ^4.1.1
|
||||
|
||||
hono@4.5.8:
|
||||
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
|
||||
hono@4.5.9:
|
||||
resolution: {integrity: sha512-zz8ktqMDRrZETjxBrv8C5PQRFbrTRCLNVAjD1SNQyOzv4VjmX68Uxw83xQ6oxdAB60HiWnGEatiKA8V3SZLDkQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
html-entities@2.5.2:
|
||||
|
|
@ -4493,8 +4493,8 @@ packages:
|
|||
svelte-lazy-loader@1.0.0:
|
||||
resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==}
|
||||
|
||||
svelte-meta-tags@3.1.3:
|
||||
resolution: {integrity: sha512-iIdJgxKdMUqFGR4m88jBE9KTSO2jdKE5CRjyRtAjdevW51jL4TtDZwL7GOtr5Fd2dw/+jyQIPD7APATP191qIA==}
|
||||
svelte-meta-tags@3.1.4:
|
||||
resolution: {integrity: sha512-TUIfhut0iVeTm7f5v/ZuU/tZ9XsNig9bNN8yK0t2x2WL9qw6AxAVRe9i5XddYJE0SuVwkoDCzjoSg5hXv7oWbQ==}
|
||||
peerDependencies:
|
||||
svelte: ^3.55.0 || ^4.0.0
|
||||
|
||||
|
|
@ -4737,8 +4737,8 @@ packages:
|
|||
tslib@2.7.0:
|
||||
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
|
||||
|
||||
tsx@4.18.0:
|
||||
resolution: {integrity: sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==}
|
||||
tsx@4.19.0:
|
||||
resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -5695,20 +5695,20 @@ snapshots:
|
|||
'@hapi/hoek': 9.3.0
|
||||
optional: true
|
||||
|
||||
'@hono/swagger-ui@0.4.1(hono@4.5.8)':
|
||||
'@hono/swagger-ui@0.4.1(hono@4.5.9)':
|
||||
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:
|
||||
'@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: 4.5.8
|
||||
'@hono/zod-validator': 0.2.2(hono@4.5.9)(zod@3.23.8)
|
||||
hono: 4.5.9
|
||||
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:
|
||||
hono: 4.5.8
|
||||
hono: 4.5.9
|
||||
zod: 3.23.8
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
|
|
@ -7562,11 +7562,11 @@ snapshots:
|
|||
|
||||
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:
|
||||
hono: 4.5.8
|
||||
hono: 4.5.9
|
||||
|
||||
hono@4.5.8: {}
|
||||
hono@4.5.9: {}
|
||||
|
||||
html-entities@2.5.2: {}
|
||||
|
||||
|
|
@ -8301,14 +8301,14 @@ snapshots:
|
|||
postcss: 8.4.41
|
||||
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:
|
||||
lilconfig: 3.1.1
|
||||
yaml: 2.4.2
|
||||
optionalDependencies:
|
||||
jiti: 1.21.6
|
||||
postcss: 8.4.41
|
||||
tsx: 4.18.0
|
||||
tsx: 4.19.0
|
||||
|
||||
postcss-logical@7.0.1(postcss@8.4.41):
|
||||
dependencies:
|
||||
|
|
@ -8878,14 +8878,14 @@ snapshots:
|
|||
|
||||
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:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
chokidar: 3.6.0
|
||||
picocolors: 1.0.0
|
||||
sade: 1.8.1
|
||||
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
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
|
|
@ -8930,7 +8930,7 @@ snapshots:
|
|||
|
||||
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:
|
||||
schema-dts: 1.1.2(typescript@5.5.4)
|
||||
svelte: 5.0.0-next.175
|
||||
|
|
@ -8941,7 +8941,7 @@ snapshots:
|
|||
dependencies:
|
||||
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:
|
||||
'@types/pug': 2.0.10
|
||||
detect-indent: 6.1.0
|
||||
|
|
@ -8951,16 +8951,16 @@ snapshots:
|
|||
svelte: 5.0.0-next.175
|
||||
optionalDependencies:
|
||||
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
|
||||
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:
|
||||
svelte: 5.0.0-next.175
|
||||
optionalDependencies:
|
||||
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
|
||||
typescript: 5.5.4
|
||||
|
||||
|
|
@ -9174,7 +9174,7 @@ snapshots:
|
|||
|
||||
tslib@2.7.0: {}
|
||||
|
||||
tsx@4.18.0:
|
||||
tsx@4.19.0:
|
||||
dependencies:
|
||||
esbuild: 0.23.0
|
||||
get-tsconfig: 4.7.5
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
</DropdownMenu.Root>
|
||||
{:else}
|
||||
<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}
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// $: termsValue = $form.terms as Writable<boolean>;
|
||||
</script>
|
||||
|
||||
<form method="POST" action="/sign-up" use:enhance>
|
||||
<form method="POST" action="/signup" use:enhance>
|
||||
<h1>Signup user</h1>
|
||||
<label class="label">
|
||||
<span class="sr-only">First Name</span>
|
||||
|
|
|
|||
7
src/lib/dtos/verify-password.dto.ts
Normal file
7
src/lib/dtos/verify-password.dto.ts
Normal 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>
|
||||
|
|
@ -11,6 +11,7 @@ import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
|
|||
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
|
||||
import { updateEmailDto } from '$lib/dtos/update-email.dto'
|
||||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { verifyPasswordDto } from '$lib/dtos/verify-password.dto'
|
||||
|
||||
@injectable()
|
||||
export class IamController implements Controller {
|
||||
|
|
@ -36,6 +37,15 @@ export class IamController implements Controller {
|
|||
}
|
||||
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) => {
|
||||
const user = c.var.user
|
||||
const { email } = c.req.valid('json')
|
||||
|
|
|
|||
39
src/lib/server/api/controllers/mfa.controller.ts
Normal file
39
src/lib/server/api/controllers/mfa.controller.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { config } from './common/config';
|
|||
import { container } from 'tsyringe';
|
||||
import { IamController } from './controllers/iam.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 {SignupController} from "$lib/server/api/controllers/signup.controller";
|
||||
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller";
|
||||
|
|
@ -44,6 +45,7 @@ const routes = app
|
|||
.route('/signup', container.resolve(SignupController).routes())
|
||||
.route('/wishlists', container.resolve(WishlistController).routes())
|
||||
.route('/collections', container.resolve(CollectionController).routes())
|
||||
.route('/mfa', container.resolve(MfaController).routes())
|
||||
.get('/', (c) => c.json({ message: 'Server is healthy' }));
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ for (const table of [
|
|||
schema.publishers,
|
||||
schema.publishersToExternalIds,
|
||||
schema.publishers_to_games,
|
||||
schema.recovery_codes,
|
||||
schema.recoveryCodesTable,
|
||||
schema.roles,
|
||||
schema.sessionsTable,
|
||||
schema.twoFactorTable,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export * from './passwordResetTokens';
|
|||
export * from './publishers';
|
||||
export * from './publishersToExternalIds';
|
||||
export * from './publishersToGames';
|
||||
export * from './recoveryCodes';
|
||||
export * from './recovery-codes.table';
|
||||
export * from './roles';
|
||||
export * from './sessions.table';
|
||||
export * from './two-factor.table';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { InferSelectModel } from 'drizzle-orm';
|
|||
import { usersTable } from './users.table';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
export const recovery_codes = pgTable('recovery_codes', {
|
||||
export const recoveryCodesTable = pgTable('recovery_codes', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
|
|
@ -13,4 +13,4 @@ export const recovery_codes = pgTable('recovery_codes', {
|
|||
...timestamps,
|
||||
});
|
||||
|
||||
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;
|
||||
export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>;
|
||||
|
|
@ -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) {
|
||||
return this.db.query.credentialsTable.findFirst({
|
||||
where: and(
|
||||
|
|
@ -59,4 +68,16 @@ export class CredentialsRepository {
|
|||
.returning()
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
|
@ -66,4 +66,13 @@ export class IamService {
|
|||
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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
src/lib/server/api/services/totp.service.ts
Normal file
36
src/lib/server/api/services/totp.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -68,4 +68,17 @@ export class UsersService {
|
|||
async findOneById(id: string) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import db from '../../../../db';
|
||||
import { wishlists } from '$db/schema';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
import { db } from '$lib/server/api/infrastructure/database';
|
||||
import { wishlists } from '$lib/server/api/infrastructure/database/tables';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
|
||||
export async function load(event) {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
try {
|
||||
const dbWishlists = await db.query.wishlists.findMany({
|
||||
where: eq(wishlists.user_id, user!.id!),
|
||||
where: eq(wishlists.user_id, authedUser.id),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -10,129 +10,129 @@ import { db } from '$lib/server/api/infrastructure/database';
|
|||
import type { PageServerLoad } from './$types';
|
||||
import { usersTable, credentialsTable } from '$lib/server/api/infrastructure/database/tables';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
import {updateProfileDto} from "$lib/dtos/update-profile.dto";
|
||||
import {updateEmailDto} from "$lib/dtos/update-email.dto";
|
||||
import { updateProfileDto } from "$lib/dtos/update-profile.dto";
|
||||
import { updateEmailDto } from "$lib/dtos/update-email.dto";
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event;
|
||||
const { locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
console.log('authedUser', authedUser);
|
||||
// if (userNotAuthenticated(user, session)) {
|
||||
// redirect(302, '/login', notSignedInMessage, event);
|
||||
// }
|
||||
// const dbUser = await db.query.usersTable.findFirst({
|
||||
// where: eq(usersTable.id, user!.id!),
|
||||
// });
|
||||
console.log('authedUser', authedUser);
|
||||
// if (userNotAuthenticated(user, session)) {
|
||||
// redirect(302, '/login', notSignedInMessage, event);
|
||||
// }
|
||||
// const dbUser = await db.query.usersTable.findFirst({
|
||||
// where: eq(usersTable.id, user!.id!),
|
||||
// });
|
||||
|
||||
const profileForm = await superValidate(zod(profileSchema), {
|
||||
defaults: {
|
||||
firstName: authedUser?.firstName ?? '',
|
||||
lastName: authedUser?.lastName ?? '',
|
||||
username: authedUser?.username ?? '',
|
||||
},
|
||||
});
|
||||
const emailForm = await superValidate(zod(changeEmailSchema), {
|
||||
defaults: {
|
||||
email: authedUser?.email ?? '',
|
||||
},
|
||||
});
|
||||
const profileForm = await superValidate(zod(profileSchema), {
|
||||
defaults: {
|
||||
firstName: authedUser?.firstName ?? '',
|
||||
lastName: authedUser?.lastName ?? '',
|
||||
username: authedUser?.username ?? '',
|
||||
},
|
||||
});
|
||||
const emailForm = await superValidate(zod(changeEmailSchema), {
|
||||
defaults: {
|
||||
email: authedUser?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
// const twoFactorDetails = await db.query.twoFactor.findFirst({
|
||||
// where: eq(twoFactor.userId, authedUser!.id!),
|
||||
// });
|
||||
// const twoFactorDetails = await db.query.twoFactor.findFirst({
|
||||
// where: eq(twoFactor.userId, authedUser!.id!),
|
||||
// });
|
||||
|
||||
return {
|
||||
profileForm,
|
||||
emailForm,
|
||||
hasSetupTwoFactor: false //!!twoFactorDetails?.enabled,
|
||||
};
|
||||
return {
|
||||
profileForm,
|
||||
emailForm,
|
||||
hasSetupTwoFactor: false //!!twoFactorDetails?.enabled,
|
||||
};
|
||||
};
|
||||
|
||||
const changeEmailIfNotEmpty = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, { message: 'Email must be less than 64 characters' })
|
||||
.email({ message: 'Please enter a valid email' }),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, { message: 'Email must be less than 64 characters' })
|
||||
.email({ message: 'Please enter a valid email' }),
|
||||
});
|
||||
|
||||
export const actions: Actions = {
|
||||
profileUpdate: async (event) => {
|
||||
const { locals } = event;
|
||||
profileUpdate: async (event) => {
|
||||
const { locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
|
||||
if (!authedUser) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
if (!authedUser) {
|
||||
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);
|
||||
console.log('data from profile update', error);
|
||||
if (error) {
|
||||
return setError(form, 'username', error);
|
||||
}
|
||||
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse);
|
||||
console.log('data from profile update', error);
|
||||
if (error) {
|
||||
return setError(form, 'username', error);
|
||||
}
|
||||
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('profile updated successfully');
|
||||
return message(form, { type: 'success', message: 'Profile updated successfully!' });
|
||||
},
|
||||
changeEmail: async (event) => {
|
||||
const form = await superValidate(event, zod(updateEmailDto));
|
||||
console.log('profile updated successfully');
|
||||
return message(form, { type: 'success', message: 'Profile updated successfully!' });
|
||||
},
|
||||
changeEmail: async (event) => {
|
||||
const form = await superValidate(event, zod(updateEmailDto));
|
||||
|
||||
const newEmail = form.data?.email;
|
||||
if (
|
||||
!form.valid ||
|
||||
!newEmail ||
|
||||
(newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)
|
||||
) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
const newEmail = form.data?.email;
|
||||
if (
|
||||
!form.valid ||
|
||||
!newEmail ||
|
||||
(newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)
|
||||
) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const user = event.locals.user;
|
||||
const existingUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, newEmail),
|
||||
});
|
||||
const user = event.locals.user;
|
||||
const existingUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, newEmail),
|
||||
});
|
||||
|
||||
if (existingUser && existingUser.id !== user.id) {
|
||||
return setError(form, 'email', 'That email is already taken');
|
||||
}
|
||||
if (existingUser && existingUser.id !== user.id) {
|
||||
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) {
|
||||
// Send email to confirm new email?
|
||||
// auth.update
|
||||
// await locals.prisma.key.update({
|
||||
// where: {
|
||||
// id: 'emailpassword:' + user.email
|
||||
// },
|
||||
// data: {
|
||||
// id: 'emailpassword:' + form.data.email
|
||||
// }
|
||||
// });
|
||||
// auth.updateUserAttributes(user.user_id, {
|
||||
// receiveEmail: false
|
||||
// });
|
||||
// }
|
||||
// if (user.email !== form.data.email) {
|
||||
// Send email to confirm new email?
|
||||
// auth.update
|
||||
// await locals.prisma.key.update({
|
||||
// where: {
|
||||
// id: 'emailpassword:' + user.email
|
||||
// },
|
||||
// data: {
|
||||
// id: 'emailpassword:' + form.data.email
|
||||
// }
|
||||
// });
|
||||
// auth.updateUserAttributes(user.user_id, {
|
||||
// receiveEmail: false
|
||||
// });
|
||||
// }
|
||||
|
||||
return message(form, { type: 'success', message: 'Email updated successfully!' });
|
||||
},
|
||||
return message(form, { type: 'success', message: 'Email updated successfully!' });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -96,16 +96,16 @@
|
|||
</form>
|
||||
<div class="mt-6">
|
||||
{#if !hasSetupTwoFactor}
|
||||
<p>Two Factor Authentication is: <strong>Disabled</strong></p>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
|
||||
<p>Multi Factor Authentication is: <strong>Disabled</strong></p>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
|
||||
<KeyRound class="mr-2 h-4 w-4" />
|
||||
Setup 2FA
|
||||
Setup Multi-factor Authentication
|
||||
</Button>
|
||||
{:else}
|
||||
<p>Two Factor Authentication is: <strong>Enabled</strong></p>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
|
||||
<p>Multi Factor Authentication is: <strong>Enabled</strong></p>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
|
||||
<KeyRound class="mr-2 h-4 w-4" />
|
||||
Disable 2FA
|
||||
Disable Multi-factor Authentication
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,30 +12,32 @@ import { redirect, setFlash } from 'sveltekit-flash-message/server';
|
|||
import type { PageServerLoad } from '../../$types';
|
||||
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import db from '$lib/server/api/infrastructure/database';
|
||||
import { recoveryCodes, twoFactor, usersTable } from '$lib/server/api/infrastructure/database/tables';
|
||||
import { db } from '$lib/server/api/infrastructure/database';
|
||||
import { recoveryCodesTable, credentialsTable, usersTable, type Credentials } from '$lib/server/api/infrastructure/database/tables';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
import env from '../../../../../../env';
|
||||
import env from '$src/env';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
|
||||
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
|
||||
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const dbUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, user!.id!),
|
||||
});
|
||||
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
|
||||
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
|
||||
// const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
|
||||
|
||||
const twoFactorDetails = await db.query.twoFactor.findFirst({
|
||||
where: eq(twoFactor.userId, dbUser!.id!),
|
||||
});
|
||||
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
||||
if (error || !data) {
|
||||
return fail(500, {
|
||||
addTwoFactorForm,
|
||||
});
|
||||
}
|
||||
const { totpCredential } = data
|
||||
|
||||
if (twoFactorDetails?.enabled) {
|
||||
if (totpCredential) {
|
||||
return {
|
||||
addTwoFactorForm,
|
||||
removeTwoFactorForm,
|
||||
|
|
@ -43,35 +45,27 @@ export const load: PageServerLoad = async (event) => {
|
|||
recoveryCodes: [],
|
||||
totpUri: '',
|
||||
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 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)
|
||||
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
|
||||
if (!createdTotpCredentials?.secret_data) {
|
||||
return fail(500, {
|
||||
addTwoFactorForm,
|
||||
})
|
||||
}
|
||||
const totpUri = createTOTPKeyURI(issuer, accountName, createdTotpCredentials.secret_data);
|
||||
|
||||
addTwoFactorForm.data = {
|
||||
current_password: '',
|
||||
|
|
@ -88,111 +82,85 @@ export const load: PageServerLoad = async (event) => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
enableTwoFactor: async (event) => {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401);
|
||||
enableTotp: async (event) => {
|
||||
const { locals } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
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) {
|
||||
return fail(400, {
|
||||
addTwoFactorForm,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
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(
|
||||
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
|
||||
if (error || !data) {
|
||||
return fail(500, {
|
||||
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) {
|
||||
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 (totpCredential.secret_data === '' || totpCredential.secret_data === null) {
|
||||
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 currentPasswordVerified = await new Argon2id().verify(
|
||||
// dbUser.hashed_password,
|
||||
addTwoFactorForm.data.current_password,
|
||||
);
|
||||
const currentPasswordVerified = await locals.api.me.verify.password.$post({
|
||||
json: { password: addTwoFactorForm.data.current_password },
|
||||
});
|
||||
|
||||
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 === '') {
|
||||
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 validOTP = await new TOTPController().verify(
|
||||
twoFactorCode,
|
||||
decodeHex(twoFactorDetails.secret),
|
||||
);
|
||||
const twoFactorCode = addTwoFactorForm.data.two_factor_code
|
||||
const validOTP = await new TOTPController().verify(twoFactorCode, decodeHex(twoFactorDetails.secret))
|
||||
|
||||
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) => {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
|
||||
disableTotp: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema))
|
||||
|
||||
if (!removeTwoFactorForm.valid) {
|
||||
return fail(400, {
|
||||
removeTwoFactorForm,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
return fail(401, {
|
||||
removeTwoFactorForm,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const dbUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, user.id),
|
||||
});
|
||||
})
|
||||
|
||||
// if (!dbUser?.hashed_password) {
|
||||
// removeTwoFactorForm.data.current_password = '';
|
||||
|
|
@ -205,24 +173,24 @@ export const actions: Actions = {
|
|||
const currentPasswordVerified = await new Argon2id().verify(
|
||||
// dbUser.hashed_password,
|
||||
removeTwoFactorForm.data.current_password,
|
||||
);
|
||||
)
|
||||
|
||||
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({
|
||||
where: eq(twoFactor.userId, dbUser.id),
|
||||
});
|
||||
})
|
||||
|
||||
if (!twoFactorDetails) {
|
||||
return fail(500, {
|
||||
removeTwoFactorForm,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
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.update(twoFactor).set({ enabled: false }).where(eq(twoFactor.userId, user.id))
|
||||
await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id))
|
||||
|
||||
// setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
|
||||
redirect(
|
||||
|
|
@ -233,6 +201,6 @@ export const actions: Actions = {
|
|||
message: 'Two-Factor Authentication has been disabled.',
|
||||
},
|
||||
event,
|
||||
);
|
||||
)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
{#if twoFactorEnabled}
|
||||
<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>
|
||||
<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.Control let:attrs>
|
||||
<Form.Label for="password">Current Password</Form.Label>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
{:else}
|
||||
<h2>Please scan the following QR Code</h2>
|
||||
<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.Control let:attrs>
|
||||
<Form.Label for="code">Enter Code</Form.Label>
|
||||
|
|
@ -1,31 +1,36 @@
|
|||
import db from '../../../../../../../db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { alphabet, generateRandomString } from 'oslo/crypto';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { db } from '$lib/server/api/infrastructure/database';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
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';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
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({
|
||||
where: eq(twoFactor.userId, dbUser!.id),
|
||||
where: eq(twoFactor.userId, dbUser.id),
|
||||
});
|
||||
|
||||
if (twoFactorDetails?.enabled) {
|
||||
const dbRecoveryCodes = await db.query.recoveryCodes.findMany({
|
||||
where: eq(recoveryCodes.userId, user!.id),
|
||||
where: eq(recoveryCodes.userId, authedUser.id),
|
||||
});
|
||||
|
||||
if (dbRecoveryCodes.length === 0) {
|
||||
|
|
@ -37,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
const hashedCode = await new Argon2id().hash(code);
|
||||
console.log('Inserting recovery code', code, hashedCode);
|
||||
await db.insert(recoveryCodes).values({
|
||||
userId: user!.id,
|
||||
userId: authedUser.id,
|
||||
code: hashedCode,
|
||||
});
|
||||
}
|
||||
|
|
@ -5,22 +5,22 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
|
|||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import type { PageServerLoad } from '../../../$types';
|
||||
import db from '../../../../../../../db';
|
||||
import { db } from '$lib/server/api/infrastructure/database';
|
||||
import { changeUserPasswordSchema } from '$lib/validations/account';
|
||||
import { lucia } from '$lib/server/auth.js';
|
||||
import { usersTable } from '$db/schema';
|
||||
import { usersTable } from '$lib/server/api/infrastructure/database/tables';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import type { Cookie } from 'lucia';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const form = await superValidate(event, zod(changeUserPasswordSchema));
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(changeUserPasswordSchema));
|
||||
|
||||
form.data = {
|
||||
current_password: '',
|
||||
password: '',
|
||||
|
|
@ -34,9 +34,10 @@ export const load: PageServerLoad = async (event) => {
|
|||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401);
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(changeUserPasswordSchema));
|
||||
|
|
@ -57,7 +58,7 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
const dbUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, user!.id),
|
||||
where: eq(usersTable.id, authedUser.id),
|
||||
});
|
||||
|
||||
// if (!dbUser?.hashed_password) {
|
||||
|
|
@ -78,14 +79,14 @@ export const actions: Actions = {
|
|||
if (!currentPasswordVerified) {
|
||||
return setError(form, 'current_password', 'Your password is incorrect');
|
||||
}
|
||||
if (user?.username) {
|
||||
if (authedUser?.username) {
|
||||
let sessionCookie: Cookie;
|
||||
try {
|
||||
if (form.data.password !== form.data.confirm_password) {
|
||||
return setError(form, 'Password and confirm password do not match');
|
||||
}
|
||||
const hashedPassword = await new Argon2id().hash(form.data.password);
|
||||
await lucia.invalidateUserSessions(user.id);
|
||||
await lucia.invalidateUserSessions(authedUser.id);
|
||||
// await db
|
||||
// .update(usersTable)
|
||||
// .set({ hashed_password: hashedPassword })
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
<AddToList {in_collection} {in_wishlist} game_id={game.id} {wishlist} {collection} />
|
||||
{:else}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
{#if $page.url.pathname !== '/login'}
|
||||
<Button href="/login" variant="ghost">Login</Button>
|
||||
{/if}
|
||||
{#if $page.url.pathname !== '/sign-up'}
|
||||
<Button href="/sign-up" variant="ghost">Sign up</Button>
|
||||
{#if $page.url.pathname !== '/signup'}
|
||||
<Button href="/signup" variant="ghost">Sign up</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="auth-marketing">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<Card.Title class="text-2xl">Signup for an account</Card.Title>
|
||||
</Card.Header>
|
||||
<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>
|
||||
<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} />
|
||||
|
|
@ -22,9 +22,10 @@ const config = {
|
|||
$assets: './src/assets',
|
||||
$components: './src/components',
|
||||
'$components/*': 'src/lib/components/*',
|
||||
$db: './src/db',
|
||||
$db: './src/lib/server/api/infrastructure/database',
|
||||
$server: './src/server',
|
||||
$lib: './src/lib',
|
||||
$src: './src',
|
||||
$state: './src/state',
|
||||
$styles: './src/styles',
|
||||
$themes: './src/themes',
|
||||
|
|
|
|||
Loading…
Reference in a new issue