Refactoring the app layout, fixing checks for session and not user to fix totp.

This commit is contained in:
Bradley Shellnut 2024-11-23 14:49:16 -08:00
parent 6e67b2d4e1
commit 0f2344c70f
46 changed files with 748 additions and 724 deletions

View file

@ -54,7 +54,7 @@
"postcss-load-config": "^5.1.0",
"postcss-preset-env": "^9.6.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.8",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.3",
@ -82,7 +82,7 @@
"@iconify-icons/line-md": "^1.2.30",
"@iconify-icons/mdi": "^1.2.48",
"@inlang/paraglide-sveltekit": "^0.11.1",
"@internationalized/date": "^3.5.6",
"@internationalized/date": "^3.6.0",
"@lucia-auth/adapter-drizzle": "^1.1.0",
"@lukeed/uuid": "^2.0.1",
"@needle-di/core": "^0.8.4",
@ -96,22 +96,22 @@
"@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2",
"@scalar/hono-api-reference": "^0.5.159",
"@scalar/hono-api-reference": "^0.5.160",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4",
"boardgamegeekclient": "^1.9.1",
"bullmq": "^5.27.0",
"bullmq": "^5.28.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cookie": "^1.0.1",
"cookie": "^1.0.2",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.7",
"drizzle-orm": "^0.36.3",
"drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2",
"handlebars": "^4.7.8",
"hono": "^4.6.10",
"hono": "^4.6.11",
"hono-pino": "^0.3.0",
"hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.4.2",

View file

@ -13,13 +13,13 @@ importers:
version: 5.1.0
'@hono/swagger-ui':
specifier: ^0.4.1
version: 0.4.1(hono@4.6.10)
version: 0.4.1(hono@4.6.11)
'@hono/zod-openapi':
specifier: ^0.15.3
version: 0.15.3(hono@4.6.10)(zod@3.23.8)
version: 0.15.3(hono@4.6.11)(zod@3.23.8)
'@hono/zod-validator':
specifier: ^0.2.2
version: 0.2.2(hono@4.6.10)(zod@3.23.8)
version: 0.2.2(hono@4.6.11)(zod@3.23.8)
'@iconify-icons/line-md':
specifier: ^1.2.30
version: 1.2.30
@ -30,8 +30,8 @@ importers:
specifier: ^0.11.1
version: 0.11.5(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))
'@internationalized/date':
specifier: ^3.5.6
version: 3.5.6
specifier: ^3.6.0
version: 3.6.0
'@lucia-auth/adapter-drizzle':
specifier: ^1.1.0
version: 1.1.0(drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(lucia@3.2.0)
@ -72,8 +72,8 @@ importers:
specifier: ^2.2.2
version: 2.2.2
'@scalar/hono-api-reference':
specifier: ^0.5.159
version: 0.5.159(hono@4.6.10)
specifier: ^0.5.160
version: 0.5.160(hono@4.6.11)
'@sveltejs/adapter-node':
specifier: ^5.2.9
version: 5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))
@ -87,8 +87,8 @@ importers:
specifier: ^1.9.1
version: 1.9.1
bullmq:
specifier: ^5.27.0
version: 5.27.0
specifier: ^5.28.1
version: 5.28.1
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@ -96,8 +96,8 @@ importers:
specifier: ^2.1.1
version: 2.1.1
cookie:
specifier: ^1.0.1
version: 1.0.1
specifier: ^1.0.2
version: 1.0.2
dotenv:
specifier: ^16.4.5
version: 16.4.5
@ -117,17 +117,17 @@ importers:
specifier: ^4.7.8
version: 4.7.8
hono:
specifier: ^4.6.10
version: 4.6.10
specifier: ^4.6.11
version: 4.6.11
hono-pino:
specifier: ^0.3.0
version: 0.3.0(hono@4.6.10)(pino@9.5.0)
version: 0.3.0(hono@4.6.11)(pino@9.5.0)
hono-rate-limiter:
specifier: ^0.4.0
version: 0.4.0(hono@4.6.10)
version: 0.4.0(hono@4.6.11)
hono-zod-openapi:
specifier: ^0.4.2
version: 0.4.2(hono@4.6.10)(zod@3.23.8)
version: 0.4.2(hono@4.6.11)(zod@3.23.8)
html-entities:
specifier: ^2.5.2
version: 2.5.2
@ -178,7 +178,7 @@ importers:
version: 0.2.2
stoker:
specifier: ^1.3.0
version: 1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8))(hono@4.6.10)(openapi3-ts@4.4.0)
version: 1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8))(hono@4.6.11)(openapi3-ts@4.4.0)
svelte-lazy-loader:
specifier: ^1.0.0
version: 1.0.0
@ -292,8 +292,8 @@ importers:
specifier: ^3.3.3
version: 3.3.3
prettier-plugin-svelte:
specifier: ^3.2.8
version: 3.2.8(prettier@3.3.3)(svelte@5.0.0-next.175)
specifier: ^3.3.0
version: 3.3.0(prettier@3.3.3)(svelte@5.0.0-next.175)
svelte:
specifier: 5.0.0-next.175
version: 5.0.0-next.175
@ -1665,8 +1665,8 @@ packages:
'@inlang/translatable@1.3.1':
resolution: {integrity: sha512-VAtle21vRpIrB+axtHFrFB0d1HtDaaNj+lV77eZQTJyOWbTFYTVIQJ8WAbyw9eu4F6h6QC2FutLyxjMomxfpcQ==}
'@internationalized/date@3.5.6':
resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==}
'@internationalized/date@3.6.0':
resolution: {integrity: sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==}
'@ioredis/commands@1.2.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
@ -2344,8 +2344,8 @@ packages:
cpu: [x64]
os: [win32]
'@scalar/hono-api-reference@0.5.159':
resolution: {integrity: sha512-nUKaN0CKvytbXPj9b6taF/efKKRqEUwhVxlfLVjrJXN0eHNHDWxG9e/5Tyw1o2MXJo1cQpGZ4qTh48k/8u6ZjA==}
'@scalar/hono-api-reference@0.5.160':
resolution: {integrity: sha512-3NXXh4B+5sa9QEchtQNKH0me4pEB8soFfCSqOxsuB0d8agokDO574cP9Qq4l6vwSh6FMiqSdaGTv5459g8hTCg==}
engines: {node: '>=18'}
peerDependencies:
hono: ^4.0.0
@ -2354,8 +2354,8 @@ packages:
resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==}
engines: {node: '>=18'}
'@scalar/types@0.0.19':
resolution: {integrity: sha512-wOxtXd35BS0DaVhBopQUB8c8hfLQ+/PKEr99GbOZW+4DWCrEB8JfWJgvpJyxHU6by7LHNVY4fvpFQR7Ezh1IIw==}
'@scalar/types@0.0.20':
resolution: {integrity: sha512-Sx7tqiuV9ZNp2XpIXKD/srzTjjb4I6aof0Y7+U5MgEuKPwrRnYS/BaocdNgWLnpCZisBIg5qp4YSqozxibabVg==}
engines: {node: '>=18'}
'@sideway/address@4.1.5':
@ -2801,8 +2801,8 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bullmq@5.27.0:
resolution: {integrity: sha512-DZWrjDLkecZZ1/43h/SkG6CxU8nO/Lq/0svVoQdw33ksUCGfccgjbvCa/cuxHP/OvhxlTAA0cO3dBOoaT7sRFQ==}
bullmq@5.28.1:
resolution: {integrity: sha512-iSoqziPLKH//mmoc4Aj3/opTmk1PgFdITwUrx/wDqrTxfBRjnTGInsu129LCEY6d+SmhZWnA9PYU6ciX+NT64A==}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
@ -2956,8 +2956,8 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
cookie@1.0.1:
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
core-js@3.38.1:
@ -3659,8 +3659,8 @@ packages:
hono: ^4.6.3
zod: ^3.21.4
hono@4.6.10:
resolution: {integrity: sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg==}
hono@4.6.11:
resolution: {integrity: sha512-f0LwJQFKdUUrCUAVowxSvNCjyzI7ZLt8XWYU/EApyeq5FfOvHFarBaE5rjU9HTNFk4RI0FkdB2edb3p/7xZjzQ==}
engines: {node: '>=16.9.0'}
hookable@5.5.3:
@ -4648,8 +4648,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier-plugin-svelte@3.2.8:
resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==}
prettier-plugin-svelte@3.3.0:
resolution: {integrity: sha512-iNoYiQUx4zwqbQDW/bk0WR75w+QiY4fHJQpGQ5v8Yr7X5m7YoSvs2buUnhoYFXNAL32ULVmrjPSc0vVOHJsO0Q==}
peerDependencies:
prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
@ -6325,25 +6325,25 @@ snapshots:
'@hapi/hoek': 9.3.0
optional: true
'@hono/swagger-ui@0.4.1(hono@4.6.10)':
'@hono/swagger-ui@0.4.1(hono@4.6.11)':
dependencies:
hono: 4.6.10
hono: 4.6.11
'@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8)':
'@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8)':
dependencies:
'@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8)
'@hono/zod-validator': 0.2.2(hono@4.6.10)(zod@3.23.8)
hono: 4.6.10
'@hono/zod-validator': 0.2.2(hono@4.6.11)(zod@3.23.8)
hono: 4.6.11
zod: 3.23.8
'@hono/zod-validator@0.2.2(hono@4.6.10)(zod@3.23.8)':
'@hono/zod-validator@0.2.2(hono@4.6.11)(zod@3.23.8)':
dependencies:
hono: 4.6.10
hono: 4.6.11
zod: 3.23.8
'@hono/zod-validator@0.4.1(hono@4.6.10)(zod@3.23.8)':
'@hono/zod-validator@0.4.1(hono@4.6.11)(zod@3.23.8)':
dependencies:
hono: 4.6.10
hono: 4.6.11
zod: 3.23.8
'@humanwhocodes/config-array@0.13.0':
@ -6571,7 +6571,7 @@ snapshots:
dependencies:
'@inlang/language-tag': 1.5.1
'@internationalized/date@3.5.6':
'@internationalized/date@3.6.0':
dependencies:
'@swc/helpers': 0.5.13
@ -6665,7 +6665,7 @@ snapshots:
dependencies:
'@floating-ui/core': 1.6.8
'@floating-ui/dom': 1.6.11
'@internationalized/date': 3.5.6
'@internationalized/date': 3.6.0
dequal: 2.0.3
focus-trap: 7.6.0
nanoid: 5.0.7
@ -6675,7 +6675,7 @@ snapshots:
dependencies:
'@floating-ui/core': 1.6.8
'@floating-ui/dom': 1.6.11
'@internationalized/date': 3.5.6
'@internationalized/date': 3.6.0
dequal: 2.0.3
focus-trap: 7.6.0
nanoid: 5.0.7
@ -7226,14 +7226,14 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.24.0':
optional: true
'@scalar/hono-api-reference@0.5.159(hono@4.6.10)':
'@scalar/hono-api-reference@0.5.160(hono@4.6.11)':
dependencies:
'@scalar/types': 0.0.19
hono: 4.6.10
'@scalar/types': 0.0.20
hono: 4.6.11
'@scalar/openapi-types@0.1.5': {}
'@scalar/types@0.0.19':
'@scalar/types@0.0.20':
dependencies:
'@scalar/openapi-types': 0.1.5
'@unhead/schema': 1.11.11
@ -7701,7 +7701,7 @@ snapshots:
bits-ui@0.21.16(svelte@5.0.0-next.175):
dependencies:
'@internationalized/date': 3.5.6
'@internationalized/date': 3.6.0
'@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175)
nanoid: 5.0.7
svelte: 5.0.0-next.175
@ -7766,7 +7766,7 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
bullmq@5.27.0:
bullmq@5.28.1:
dependencies:
cron-parser: 4.9.0
ioredis: 5.4.1
@ -7921,7 +7921,7 @@ snapshots:
cookie@0.6.0: {}
cookie@1.0.1: {}
cookie@1.0.2: {}
core-js@3.38.1: {}
@ -8636,24 +8636,24 @@ snapshots:
help-me@5.0.0: {}
hono-pino@0.3.0(hono@4.6.10)(pino@9.5.0):
hono-pino@0.3.0(hono@4.6.11)(pino@9.5.0):
dependencies:
defu: 6.1.4
hono: 4.6.10
hono: 4.6.11
pino: 9.5.0
hono-rate-limiter@0.4.0(hono@4.6.10):
hono-rate-limiter@0.4.0(hono@4.6.11):
dependencies:
hono: 4.6.10
hono: 4.6.11
hono-zod-openapi@0.4.2(hono@4.6.10)(zod@3.23.8):
hono-zod-openapi@0.4.2(hono@4.6.11)(zod@3.23.8):
dependencies:
'@hono/zod-validator': 0.4.1(hono@4.6.10)(zod@3.23.8)
hono: 4.6.10
'@hono/zod-validator': 0.4.1(hono@4.6.11)(zod@3.23.8)
hono: 4.6.11
zod: 3.23.8
zod-openapi: 3.3.0(zod@3.23.8)
hono@4.6.10: {}
hono@4.6.11: {}
hookable@5.5.3: {}
@ -9634,7 +9634,7 @@ snapshots:
prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@5.0.0-next.175):
prettier-plugin-svelte@3.3.0(prettier@3.3.3)(svelte@5.0.0-next.175):
dependencies:
prettier: 3.3.3
svelte: 5.0.0-next.175
@ -9962,13 +9962,13 @@ snapshots:
std-env@3.7.0: {}
stoker@1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8))(hono@4.6.10)(openapi3-ts@4.4.0):
stoker@1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8))(hono@4.6.11)(openapi3-ts@4.4.0):
dependencies:
'@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8)
hono: 4.6.10
hono: 4.6.11
openapi3-ts: 4.4.0
optionalDependencies:
'@hono/zod-openapi': 0.15.3(hono@4.6.10)(zod@3.23.8)
'@hono/zod-openapi': 0.15.3(hono@4.6.11)(zod@3.23.8)
string-width@4.2.3:
dependencies:

5
src/app.d.ts vendored
View file

@ -1,5 +1,6 @@
import type { ApiClient } from '$lib/server/api';
import type { Users } from '$lib/server/api/databases/postgres/tables';
import type { Session } from '$lib/server/api/services/sessions.service';
import type { parseApiResponse } from '$lib/utils/api';
// See https://svelte.dev/docs/kit/types#app.d.ts
@ -16,8 +17,8 @@ declare global {
interface Locals {
api: ApiClient['api'];
parseApiResponse: typeof parseApiResponse;
getAuthedUser: () => Promise<Returned<Users> | null>;
getAuthedUserOrThrow: () => Promise<Returned<User>>;
getAuthedUser: () => Promise<Returned<Record<Users, Session>> | null>;
getAuthedUserOrThrow: () => Promise<Returned<Record<Users, Session>>>;
}
namespace Superforms {
type Message = {

View file

@ -21,12 +21,12 @@ const apiClient: Handle = async ({ event, resolve }) => {
/* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse);
return data?.user;
const { data } = await api.me.$get().then(parseApiResponse);
return { user: data?.user, session: data?.session };
}
async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse);
const { data } = await api.me.$get().then(parseApiResponse);
if (!data || !data.user) {
throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/");
}

View file

@ -1,4 +1,3 @@
import type { Sessions } from '$lib/server/api/databases/postgres/tables';
import type { Session } from '$lib/server/api/services/sessions.service';
import type { Hono } from 'hono';
import type { PinoLogger } from 'hono-pino';

View file

@ -1,39 +1,39 @@
import {StatusCodes} from '$lib/constants/status-codes';
import {Controller} from '$lib/server/api/common/types/controller';
import {allCollections, getCollectionByCUID, numberOfCollections} from '$lib/server/api/controllers/collection.routes';
import {CollectionsService} from '$lib/server/api/services/collections.service';
import {openApi} from 'hono-zod-openapi';
import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller';
import { allCollections, getCollectionByCUID, numberOfCollections } from '$lib/server/api/controllers/collection.routes';
import { CollectionsService } from '$lib/server/api/services/collections.service';
import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware';
import { requireFullAuth } from '../middleware/require-auth.middleware';
@injectable()
export class CollectionController extends Controller {
constructor(private collectionsService = inject(CollectionsService)) {
super();
}
constructor(private collectionsService = inject(CollectionsService)) {
super();
}
routes() {
return this.controller
.get('/', requireAuth, openApi(allCollections), async (c) => {
const user = c.var.user;
const collections = await this.collectionsService.findAllByUserId(user.id);
console.log('collections service', collections);
return c.json({ collections }, StatusCodes.OK);
})
.get('/count', requireAuth, openApi(numberOfCollections), async (c) => {
const user = c.var.user;
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id);
return c.json({ count: collections?.length || 0 }, StatusCodes.OK);
})
.get('/:cuid', requireAuth, openApi(getCollectionByCUID), async (c) => {
const cuid = c.req.param('cuid');
const collection = await this.collectionsService.findOneByCuid(cuid);
routes() {
return this.controller
.get('/', requireFullAuth, openApi(allCollections), async (c) => {
const user = c.var.user;
const collections = await this.collectionsService.findAllByUserId(user.id);
console.log('collections service', collections);
return c.json({ collections }, StatusCodes.OK);
})
.get('/count', requireFullAuth, openApi(numberOfCollections), async (c) => {
const user = c.var.user;
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id);
return c.json({ count: collections?.length || 0 }, StatusCodes.OK);
})
.get('/:cuid', requireFullAuth, openApi(getCollectionByCUID), async (c) => {
const cuid = c.req.param('cuid');
const collection = await this.collectionsService.findOneByCuid(cuid);
if (!collection) {
return c.json('Collection not found', StatusCodes.NOT_FOUND);
}
if (!collection) {
return c.json('Collection not found', StatusCodes.NOT_FOUND);
}
return c.json({ collection }, StatusCodes.OK);
});
}
return c.json({ collection }, StatusCodes.OK);
});
}
}

View file

@ -1,116 +1,119 @@
import {StatusCodes} from '$lib/constants/status-codes';
import {Controller} from '$lib/server/api/common/types/controller';
import {createBlankSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies';
import {changePasswordDto} from '$lib/server/api/dtos/change-password.dto';
import {updateEmailDto} from '$lib/server/api/dtos/update-email.dto';
import {updateProfileDto} from '$lib/server/api/dtos/update-profile.dto';
import {verifyPasswordDto} from '$lib/server/api/dtos/verify-password.dto';
import {limiter} from '$lib/server/api/middleware/rate-limiter.middleware';
import {IamService} from '$lib/server/api/services/iam.service';
import {LoginRequestsService} from '$lib/server/api/services/loginrequest.service';
import {SessionsService} from '$lib/server/api/services/sessions.service';
import {zValidator} from '@hono/zod-validator';
import {openApi} from 'hono-zod-openapi';
import { injectable, inject } from "@needle-di/core";
import {requireAuth} from '../middleware/require-auth.middleware';
import {iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword} from './iam.routes';
import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller';
import { createBlankSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import { IamService } from '$lib/server/api/services/iam.service';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from '@needle-di/core';
import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes';
import { UsersRepository } from '../repositories/users.repository';
@injectable()
export class IamController extends Controller {
constructor(
private iamService = inject(IamService),
private loginRequestService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
) {
super();
}
constructor(
private iamService = inject(IamService),
private loginRequestService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
private usersRepository = inject(UsersRepository),
) {
super();
}
routes() {
return this.controller
.get('/', requireAuth, openApi(iam), async (c) => {
const user = c.var.user;
return c.json({ user });
})
.put(
'/update/profile',
requireAuth,
openApi(updateProfile),
zValidator('json', updateProfileDto),
limiter({ limit: 30, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { firstName, lastName, username } = c.req.valid('json');
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
if (!updatedUser) {
return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY);
}
return c.json({ user: updatedUser }, StatusCodes.OK);
},
)
.post(
'/verify/password',
requireAuth,
zValidator('json', verifyPasswordDto),
openApi(verifyPassword),
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) {
console.log('Incorrect password');
return c.json('Incorrect password', StatusCodes.FORBIDDEN);
}
return c.json({}, StatusCodes.OK);
},
)
.put(
'/update/password',
requireAuth,
openApi(updatePassword),
zValidator('json', changePasswordDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { password, confirm_password } = c.req.valid('json');
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.sessionsService.invalidateSession(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, false);
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
} catch (error) {
console.error('Error updating password', error);
return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR);
}
},
)
.post(
'/update/email',
requireAuth,
openApi(updateEmail),
zValidator('json', updateEmailDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { email } = c.req.valid('json');
const updatedUser = await this.iamService.updateEmail(user.id, { email });
if (!updatedUser) {
return c.json('Cannot change email address', StatusCodes.FORBIDDEN);
}
return c.json({ user: updatedUser }, StatusCodes.OK);
},
)
.post('/logout', requireAuth, openApi(logout), async (c) => {
const sessionId = c.var.session.id;
await this.iamService.logout(sessionId);
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
});
}
routes() {
return this.controller
.get('/', openApi(iam), async (c) => {
const session = c.var.session;
const user = session ? await this.usersRepository.findOneByIdOrThrow(session.userId) : null;
return c.json({ user, session });
})
.put(
'/update/profile',
requireFullAuth,
openApi(updateProfile),
zValidator('json', updateProfileDto),
limiter({ limit: 30, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { firstName, lastName, username } = c.req.valid('json');
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
if (!updatedUser) {
return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY);
}
return c.json({ user: updatedUser }, StatusCodes.OK);
},
)
.post(
'/verify/password',
requireFullAuth,
zValidator('json', verifyPasswordDto),
openApi(verifyPassword),
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) {
console.log('Incorrect password');
return c.json('Incorrect password', StatusCodes.FORBIDDEN);
}
return c.json({}, StatusCodes.OK);
},
)
.put(
'/update/password',
requireFullAuth,
openApi(updatePassword),
zValidator('json', changePasswordDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { password, confirm_password } = c.req.valid('json');
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.sessionsService.invalidateSession(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, false, false);
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
} catch (error) {
console.error('Error updating password', error);
return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR);
}
},
)
.post(
'/update/email',
requireFullAuth,
openApi(updateEmail),
zValidator('json', updateEmailDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { email } = c.req.valid('json');
const updatedUser = await this.iamService.updateEmail(user.id, { email });
if (!updatedUser) {
return c.json('Cannot change email address', StatusCodes.FORBIDDEN);
}
return c.json({ user: updatedUser }, StatusCodes.OK);
},
)
.post('/logout', requireFullAuth, openApi(logout), async (c) => {
const sessionId = c.var.session.id;
await this.iamService.logout(sessionId);
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
});
}
}

View file

@ -1,37 +1,37 @@
import {Controller} from '$lib/server/api/common/types/controller';
import {cookieExpiresAt, createSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies';
import {signinUsernameDto} from '$lib/server/api/dtos/signin-username.dto';
import {SessionsService} from '$lib/server/api/services/sessions.service';
import {zValidator} from '@hono/zod-validator';
import {openApi} from 'hono-zod-openapi';
import { Controller } from '$lib/server/api/common/types/controller';
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi';
import { inject, injectable } from '@needle-di/core';
import {limiter} from '../middleware/rate-limiter.middleware';
import {LoginRequestsService} from '../services/loginrequest.service';
import {signinUsername} from './login.routes';
import { limiter } from '../middleware/rate-limiter.middleware';
import { LoginRequestsService } from '../services/loginrequest.service';
import { signinUsername } from './login.routes';
@injectable()
export class LoginController extends Controller {
constructor(
private loginRequestsService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
) {
super();
}
constructor(
private loginRequestsService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
) {
super();
}
routes() {
return this.controller.post(
'/',
openApi(signinUsername),
zValidator('json', signinUsernameDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const { username, password } = c.req.valid('json');
const session = await this.loginRequestsService.verify({ username, password }, c.req);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
},
);
}
routes() {
return this.controller.post(
'/',
openApi(signinUsername),
zValidator('json', signinUsernameDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const { username, password } = c.req.valid('json');
const session = await this.loginRequestsService.verify({ username, password }, c.req);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
},
);
}
}

View file

@ -7,7 +7,7 @@ import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from '@needle-di/core';
import { CredentialsType } from '../databases/postgres/tables';
import { requireAuth } from '../middleware/require-auth.middleware';
import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
import { createTwoFactorSchema } from '../dtos/create-totp.dto';
import { decodeBase64 } from '@oslojs/encoding';
import { LoginRequestsService } from '../services/loginrequest.service';
@ -26,12 +26,13 @@ export class MfaController extends Controller {
routes() {
return this.controller
.get('/totp', requireAuth, async (c) => {
.get('/totp', requireTempAuth, async (c) => {
const user = c.var.user;
c.var.logger.info(`The user ${user.id} is requesting TOTP credentials`);
const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential });
})
.post('/totp', requireAuth, zValidator('json', createTwoFactorSchema), async (c) => {
.post('/totp', requireTempAuth, zValidator('json', createTwoFactorSchema), async (c) => {
const user = c.var.user;
const { key } = c.req.valid('json');
const totpCredential = await this.totpService.create(user.id, decodeBase64(key));
@ -41,7 +42,7 @@ export class MfaController extends Controller {
}
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
})
.delete('/totp', requireAuth, async (c) => {
.delete('/totp', requireFullAuth, async (c) => {
const user = c.var.user;
try {
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP);
@ -54,20 +55,20 @@ export class MfaController extends Controller {
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.get('/totp/recoveryCodes', requireAuth, async (c) => {
.get('/totp/recoveryCodes', requireFullAuth, async (c) => {
const user = c.var.user;
// You can only view recovery codes once and that is on creation
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
if (existingCodes && existingCodes.length > 0) {
console.log('Recovery Codes found', existingCodes);
// Filter out codes that are not used and only return the code
const codes = existingCodes.filter(code => !code.used).map(code => code.code);
const codes = existingCodes.filter((code) => !code.used).map((code) => code.code);
return c.json({ recoveryCodes: codes });
}
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
return c.json({ recoveryCodes });
})
.post('/totp/recoveryCodes', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
.post('/totp/recoveryCodes', requireFullAuth, zValidator('json', verifyTotpDto), async (c) => {
try {
const user = c.var.user;
const { code } = c.req.valid('json');
@ -82,7 +83,7 @@ export class MfaController extends Controller {
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
.post('/totp/verify', requireTempAuth, zValidator('json', verifyTotpDto), async (c) => {
try {
const user = c.var.user;
const { code } = c.req.valid('json');
@ -90,7 +91,7 @@ export class MfaController extends Controller {
const verified = await this.totpService.verify(user.id, code);
if (verified) {
await this.usersService.updateUser(user.id, { mfa_enabled: true });
const session = await this.loginRequestService.createUserSession(user.id, c.req, true);
const session = await this.loginRequestService.createUserSession(user.id, c.req, true, true);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie);
setSessionCookie(c, sessionCookie);

View file

@ -34,7 +34,7 @@ export class SignupController extends Controller {
return c.body('Failed to create user', 500);
}
const session = await this.loginRequestService.createUserSession(user.id, c.req, false);
const session = await this.loginRequestService.createUserSession(user.id, c.req, false, false);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie);
setSessionCookie(c, sessionCookie);

View file

@ -1,29 +1,30 @@
import {Controller} from '$lib/server/api/common/types/controller';
import {UsersService} from '$lib/server/api/services/users.service';
import {inject, injectable} from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware';
import { Controller } from '$lib/server/api/common/types/controller';
import { UsersService } from '$lib/server/api/services/users.service';
import { inject, injectable } from '@needle-di/core';
import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
@injectable()
export class UserController extends Controller {
constructor(private usersService = inject(UsersService)) {
super();
}
constructor(private usersService = inject(UsersService)) {
super();
}
routes() {
return this.controller
.get('/', async (c) => {
const user = c.var.user;
return c.json({ user });
})
.get('/:id', requireAuth, async (c) => {
const id = c.req.param('id');
const user = await this.usersService.findOneById(id);
return c.json({ user });
})
.get('/username/:userName', requireAuth, async (c) => {
const userName = c.req.param('userName');
const user = await this.usersService.findOneByUsername(userName);
return c.json({ user });
});
}
routes() {
return this.controller
.get('/', requireTempAuth, async (c) => {
const session = c.var.session;
const user = session ? await this.usersService.findOneById(session.userId) : null;
return c.json({ user, session });
})
.get('/:id', requireFullAuth, async (c) => {
const id = c.req.param('id');
const user = await this.usersService.findOneById(id);
return c.json({ user });
})
.get('/username/:userName', requireFullAuth, async (c) => {
const userName = c.req.param('userName');
const user = await this.usersService.findOneByUsername(userName);
return c.json({ user });
});
}
}

View file

@ -1,25 +1,25 @@
import {Controller} from '$lib/server/api/common/types/controller'
import {WishlistsService} from '$lib/server/api/services/wishlists.service'
import {inject, injectable} from '@needle-di/core'
import {requireAuth} from '../middleware/require-auth.middleware'
import { Controller } from '$lib/server/api/common/types/controller';
import { WishlistsService } from '$lib/server/api/services/wishlists.service';
import { inject, injectable } from '@needle-di/core';
import { requireFullAuth } from '../middleware/require-auth.middleware';
@injectable()
export class WishlistController extends Controller {
constructor(private wishlistsService = inject(WishlistsService)) {
super()
}
constructor(private wishlistsService = inject(WishlistsService)) {
super();
}
routes() {
return this.controller
.get('/', requireAuth, async (c) => {
const user = c.var.user
const wishlists = await this.wishlistsService.findAllByUserId(user.id)
return c.json({ wishlists })
})
.get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid')
const wishlist = await this.wishlistsService.findOneByCuid(cuid)
return c.json({ wishlist })
})
}
routes() {
return this.controller
.get('/', requireFullAuth, async (c) => {
const user = c.var.user;
const wishlists = await this.wishlistsService.findAllByUserId(user.id);
return c.json({ wishlists });
})
.get('/:cuid', requireFullAuth, async (c) => {
const cuid = c.req.param('cuid');
const wishlist = await this.wishlistsService.findOneByCuid(cuid);
return c.json({ wishlist });
});
}
}

View file

@ -36,7 +36,6 @@ for (const table of [
schema.publishers_to_games,
schema.recoveryCodesTable,
schema.rolesTable,
schema.sessionsTable,
schema.twoFactorTable,
schema.user_roles,
schema.usersTable,

View file

@ -18,7 +18,6 @@ export * from './publishersToExternalIds.table'
export * from './publishersToGames.table'
export * from './recovery-codes.table'
export * from './roles.table'
export * from './sessions.table'
export * from './two-factor.table'
export * from './userRoles.table'
export * from './users.table'

View file

@ -1,28 +0,0 @@
import {type InferSelectModel, relations} from 'drizzle-orm';
import {boolean, pgTable, text, timestamp, uuid} from 'drizzle-orm/pg-core';
import {cuid2} from '../../../common/utils/table';
import {usersTable} from './users.table';
export const sessionsTable = pgTable('sessions', {
id: cuid2().primaryKey(),
userId: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
expiresAt: timestamp({
withTimezone: true,
mode: 'date',
}).notNull(),
ipCountry: text(),
ipAddress: text(),
twoFactorAuthEnabled: boolean().default(false),
isTwoFactorAuthenticated: boolean().default(false),
});
export const sessionsRelations = relations(sessionsTable, ({ one }) => ({
user: one(usersTable, {
fields: [sessionsTable.userId],
references: [usersTable.id],
}),
}));
export type Sessions = InferSelectModel<typeof sessionsTable>;

View file

@ -1,16 +1,30 @@
import {Unauthorized} from '$lib/server/api/common/exceptions';
import type {Sessions} from '$lib/server/api/databases/postgres/tables';
import type {MiddlewareHandler} from 'hono';
import {createMiddleware} from 'hono/factory';
import type {User} from 'lucia';
import { Unauthorized } from '$lib/server/api/common/exceptions';
import type { Sessions, Users } from '$lib/server/api/databases/postgres/tables';
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
export const requireAuth: MiddlewareHandler<{
export const requireFullAuth: MiddlewareHandler<{
Variables: {
session: Sessions;
user: Users;
};
}> = createMiddleware(async (c, next) => {
const session = c.var.session;
if (!session || (session?.twoFactorAuthEnabled && !session?.twoFactorVerified)) {
throw Unauthorized('You must be logged in to access this resource');
}
return next();
});
export const requireTempAuth: MiddlewareHandler<{
Variables: {
session: Sessions;
user: User;
user: Users;
};
}> = createMiddleware(async (c, next) => {
const user = c.var.user;
if (!user) throw Unauthorized('You must be logged in to access this resource');
const session = c.var.session;
if (!session) {
throw Unauthorized('You must be logged in to access this resource');
}
return next();
});
});

View file

@ -2,7 +2,7 @@ import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto
import { SessionsService } from '$lib/server/api/services/sessions.service';
import type { HonoRequest } from 'hono';
import { inject, injectable } from '@needle-di/core';
import { BadRequest } from '../common/exceptions';
import { BadRequest, NotFound } from '../common/exceptions';
import type { Credentials } from '../databases/postgres/tables';
import { CredentialsRepository } from '../repositories/credentials.repository';
import { UsersRepository } from '../repositories/users.repository';
@ -39,7 +39,7 @@ export class LoginRequestsService {
const existingUser = await this.usersRepository.findOneByUsername(data.username);
if (!existingUser) {
throw BadRequest('User not found');
throw NotFound('User not found');
}
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
@ -54,10 +54,15 @@ export class LoginRequestsService {
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
return await this.createUserSession(existingUser.id, req, !!totpCredentials && totpCredentials.secret_data !== null && totpCredentials.secret_data !== '');
return await this.createUserSession(
existingUser.id,
req,
!!totpCredentials && totpCredentials.secret_data !== null && totpCredentials.secret_data !== '',
false,
);
}
async createUserSession(existingUserId: string, req: HonoRequest, twoFactorAuthEnabled: boolean) {
async createUserSession(existingUserId: string, req: HonoRequest, twoFactorAuthEnabled: boolean, twoFactorVerified = false) {
const requestIpAddress = req.header('X-Forwarded-For');
const requestIpCountry = req.header('x-vercel-ip-country');
return this.sessionsService.createSession(
@ -66,7 +71,7 @@ export class LoginRequestsService {
requestIpCountry || 'unknown',
requestIpAddress || 'unknown',
twoFactorAuthEnabled,
false,
twoFactorVerified,
);
}

View file

@ -5,15 +5,36 @@ import type {Disposable} from 'tsyringe';
@injectable()
export class RedisService implements Disposable {
readonly client: Redis
readonly client: Redis;
constructor() {
this.client = new Redis(config.redis.url, {
maxRetriesPerRequest: null,
})
}
constructor() {
this.client = new Redis(config.redis.url, {
maxRetriesPerRequest: null,
});
}
async dispose(): Promise<void> {
this.client.disconnect()
}
async get(data: { prefix: string; key: string }): Promise<string | null> {
return this.client.get(`${data.prefix}:${data.key}`);
}
async set(data: { prefix: string; key: string; value: string }): Promise<void> {
await this.client.set(`${data.prefix}:${data.key}`, data.value);
}
async delete(data: { prefix: string; key: string }): Promise<void> {
await this.client.del(`${data.prefix}:${data.key}`);
}
async setWithExpiry(data: {
prefix: string;
key: string;
value: string;
expiry: number;
}): Promise<void> {
await this.client.set(`${data.prefix}:${data.key}`, data.value, 'EXAT', Math.floor(data.expiry));
}
async dispose(): Promise<void> {
this.client.disconnect();
}
}

View file

@ -17,119 +17,119 @@ export type RedisSession = {
};
export type Session = {
id: string;
userId: string;
expiresAt: Date;
ipCountry: string;
ipAddress: string;
twoFactorAuthEnabled: boolean;
isTwoFactorAuthenticated: boolean;
id: string;
userId: string;
expiresAt: Date;
ipCountry: string;
ipAddress: string;
twoFactorEnabled: boolean;
twoFactorVerified: boolean;
};
export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined };
@injectable()
export class SessionsService {
constructor(
private redisService = inject(RedisService),
private usersRepository = inject(UsersRepository),
) {}
constructor(
private redisService = inject(RedisService),
private usersRepository = inject(UsersRepository),
) {}
generateSessionToken() {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes);
}
generateSessionToken() {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes);
}
async createSession(
token: string,
userId: string,
ipCountry: string,
ipAddress: string,
twoFactorAuthEnabled: boolean,
isTwoFactorAuthenticated: boolean,
) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session = {
id: sessionId,
userId,
expiresAt: cookieExpiresAt,
ipCountry,
ipAddress,
twoFactorAuthEnabled,
isTwoFactorAuthenticated,
};
await this.redisService.client.set(
`session:${sessionId}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: session.expiresAt,
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
return session;
}
async createSession(
token: string,
userId: string,
ipCountry: string,
ipAddress: string,
twoFactorAuthEnabled: boolean,
twoFactorVerified: boolean,
) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session = {
id: sessionId,
userId,
expiresAt: cookieExpiresAt,
ipCountry,
ipAddress,
twoFactorAuthEnabled,
twoFactorVerified,
};
await this.redisService.client.set(
`session:${sessionId}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: session.expiresAt,
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.twoFactorVerified,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
return session;
}
async validateSessionToken(token: string): Promise<SessionValidationResult> {
// TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm
// const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const item = await this.redisService.client.get(`session:${token}`);
if (item === null) {
return {
session: null,
user: null,
};
}
const result: RedisSession = JSON.parse(item);
const session: Session = {
id: result.id,
userId: result.user_id,
expiresAt: new Date(result.expires_at * 1000),
ipCountry: result.ip_country,
ipAddress: result.ip_address,
twoFactorAuthEnabled: result.two_factor_auth_enabled,
isTwoFactorAuthenticated: result.is_two_factor_authenticated,
};
let user: Users | undefined = undefined;
if (session.userId) {
user = await this.usersRepository.findOneById(session.userId);
}
if (Date.now() >= session.expiresAt.getTime()) {
await this.redisService.client.del(`session:${token}`);
return {
session: null,
user: null,
};
}
async validateSessionToken(token: string): Promise<SessionValidationResult> {
// TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm
// const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const item = await this.redisService.client.get(`session:${token}`);
if (item === null) {
return {
session: null,
user: null,
};
}
const result: RedisSession = JSON.parse(item);
const session: Session = {
id: result.id,
userId: result.user_id,
expiresAt: new Date(result.expires_at * 1000),
ipCountry: result.ip_country,
ipAddress: result.ip_address,
twoFactorEnabled: result.two_factor_auth_enabled,
twoFactorVerified: result.is_two_factor_authenticated,
};
let user: Users | undefined = undefined;
if (session.userId) {
user = await this.usersRepository.findOneById(session.userId);
}
if (Date.now() >= session.expiresAt.getTime()) {
await this.redisService.client.del(`session:${token}`);
return {
session: null,
user: null,
};
}
if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) {
session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
await this.redisService.client.set(
`session:${token}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: Math.floor(session.expiresAt.getTime() / 1000),
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
}
if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) {
session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
await this.redisService.client.set(
`session:${token}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: Math.floor(session.expiresAt.getTime() / 1000),
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_enabled: session.twoFactorEnabled,
two_factor_verified: session.twoFactorVerified,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
}
return { session, user };
}
return { session, user };
}
async invalidateSession(sessionId: string) {
await this.redisService.client.del(`session:${sessionId}`);
}
async invalidateSession(sessionId: string) {
await this.redisService.client.del(`session:${sessionId}`);
}
}

View file

@ -1,19 +1,20 @@
import {eq} from 'drizzle-orm';
import {generateIdFromEntropySize, type Session, type User} from 'lucia';
import {createDate, TimeSpan} from 'oslo';
import {password_reset_tokens} from './api/databases/postgres/tables';
import {db} from './api/packages/drizzle';
import { eq } from 'drizzle-orm';
import { generateIdFromEntropySize } from 'lucia';
import { createDate, TimeSpan } from 'oslo';
import { password_reset_tokens, type Users } from './api/databases/postgres/tables';
import { db } from './api/packages/drizzle';
import type { Session } from './api/services/sessions.service';
export async function createPasswordResetToken(userId: string): Promise<string> {
// optionally invalidate all existing tokens
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId));
const tokenId = generateIdFromEntropySize(40);
await db.insert(password_reset_tokens).values({
id: tokenId,
user_id: userId,
expires_at: createDate(new TimeSpan(2, 'h')),
});
return tokenId;
// optionally invalidate all existing tokens
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId));
const tokenId = generateIdFromEntropySize(40);
await db.insert(password_reset_tokens).values({
id: tokenId,
user_id: userId,
expires_at: createDate(new TimeSpan(2, 'h')),
});
return tokenId;
}
/**
@ -23,8 +24,8 @@ export async function createPasswordResetToken(userId: string): Promise<string>
* @param session - The session object.
* @returns True if the user is not fully authenticated, otherwise false.
*/
export function userNotFullyAuthenticated(user: User | null, session: Session | null) {
return user && session && session.isTwoFactorAuthEnabled && !session.isTwoFactorAuthenticated;
export function userNotFullyAuthenticated(user: Users | null, session: Session | null) {
return user && session && session.twoFactorEnabled && !session.twoFactorVerified;
}
/**
@ -35,7 +36,7 @@ export function userNotFullyAuthenticated(user: User | null, session: Session |
* @returns {boolean} True if the user is not fully authenticated, otherwise false.
*/
export function userNotAuthenticated(user: User | null, session: Session | null) {
return !user || !session || userNotFullyAuthenticated(user, session);
return !user || !session || userNotFullyAuthenticated(user, session);
}
/**
@ -46,5 +47,5 @@ export function userNotAuthenticated(user: User | null, session: Session | null)
* @returns {boolean} True if the user is fully authenticated, otherwise false.
*/
export function userFullyAuthenticated(user: User | null, session: Session | null) {
return !userNotAuthenticated(user, session);
return !userNotAuthenticated(user, session);
}

View file

@ -0,0 +1,28 @@
import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from '../$types';
import { redirect } from 'sveltekit-flash-message/server';
import { notSignedInMessage } from '$lib/flashMessages';
export const load: LayoutServerLoad = loadFlash(async (event) => {
const { locals, url } = event;
const { user, session } = await locals.getAuthedUser();
console.log('User from protected route', user);
console.log('Session from protected route', session);
if (session === null) {
throw redirect(302, '/landing', notSignedInMessage, event);
}
if (session?.twoFactorEnabled && !session?.twoFactorVerified) {
throw redirect(302, '/login', notSignedInMessage, event);
}
return {
url: url.pathname,
user: {
cuid: user?.cuid,
firstName: user?.firstName,
lastName: user?.lastName,
username: user?.username,
},
};
});

View file

@ -0,0 +1,52 @@
import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from '../$types';
import { notSignedInMessage } from '$lib/flashMessages';
import { redirect } from 'sveltekit-flash-message/server';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
if (wishlistsError || collectionsError) {
return fail(500);
}
console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', collectionsData.collections);
return {
metaTagsChild: metaTags,
wishlists: wishlistsData.wishlists,
collections: collectionsData.collections,
};
};

View file

@ -1,22 +1,25 @@
<script lang="ts">
const { data } = $props()
const { user, wishlists = [], collections = [] } = data
import { page } from "$app/stores";
import type { PageData } from './$types';
const welcomeName = $derived.by(() => {
let welcomeName = ''
if (data?.user?.firstName) {
welcomeName += data?.user?.firstName
}
if (data?.user?.lastName) {
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName
}
let { data }: { data: PageData } = $props();
const { user, wishlists = [], collections = [] } = data;
if (welcomeName.length === 0) {
return user?.username
}
const welcomeName = $derived.by(() => {
let welcomeName = "";
if (data?.user?.firstName) {
welcomeName += data?.user?.firstName;
}
if (data?.user?.lastName) {
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName;
}
return welcomeName
})
if (welcomeName.length === 0) {
return user?.username;
}
return welcomeName;
});
</script>
<div class="container">
@ -37,7 +40,10 @@ const welcomeName = $derived.by(() => {
{:else}
<h1>Welcome to Bored Game!</h1>
<h2>Track the board games you own, the ones you want, and whether you play them enough.</h2>
<p>Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.</p>
<p>
Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an
account.
</p>
{/if}
</div>

View file

@ -0,0 +1,39 @@
import { fail, redirect } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from '../$types';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
return {
metaTagsChild: metaTags,
};
};

View file

@ -0,0 +1,26 @@
<script lang="ts">
const { data } = $props();
</script>
<div class="container">
<h1>Welcome to Bored Game!</h1>
<h2>Track the board games you own, the ones you want, and whether you play them enough.</h2>
<p>
Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.
</p>
</div>
<style lang="postcss">
a {
text-decoration: underline;
}
.container {
display: grid;
place-content: center;
max-width: 900px;
gap: 0.25rem;
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -3,10 +3,15 @@ import type { LayoutServerLoad } from '../$types';
export const load: LayoutServerLoad = loadFlash(async (event) => {
const { url, locals } = event;
const authedUser = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
return {
url: url.pathname,
authedUser,
user: user ? {
cuid: user?.cuid,
firstName: user?.firstName,
lastName: user?.lastName,
username: user?.username,
} : null,
};
});

View file

@ -7,7 +7,7 @@ const { data, children } = $props();
</script>
<div class="flex min-h-screen w-full flex-col">
<Header user={data.authedUser} />
<Header user={data.user} />
<main
class="flex min-h-[calc(100vh-theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"

View file

@ -1,63 +0,0 @@
import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const authedUser = await locals.getAuthedUser();
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
if (authedUser) {
const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
if (wishlistsError || collectionsError) {
return fail(500, 'Failed to fetch wishlistsTable or collections');
}
console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', collectionsData.collections);
return {
metaTagsChild: metaTags,
user: {
firstName: authedUser?.firstName,
lastName: authedUser?.lastName,
username: authedUser?.username,
},
wishlists: wishlistsData.wishlists,
collections: collectionsData.collections,
};
}
console.log('Not Authed');
return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] };
};

View file

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement...
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod
export const prerender = true;

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,8 +1,11 @@
import { redirect } from '@sveltejs/kit';
export async function load(event) {
const { url, locals } = event;
const { user, session } = await locals.getAuthedUser();
return {
url: url.pathname,
user: locals.user,
user,
};
}

View file

@ -9,9 +9,9 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
if (authedUser) {
if (user) {
console.log('user already signed in');
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
@ -28,9 +28,9 @@ export const actions: Actions = {
default: async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
if (authedUser) {
if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
@ -38,8 +38,10 @@ export const actions: Actions = {
const form = await superValidate(event, zod(signinUsernameDto));
const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse);
console.log('Login error', error);
if (error) {
return setError(form, 'username', error);
console.log('Setting error');
return setError(form, 'username', 'An error occurred while logging in.');
}
if (!form.valid) {

View file

@ -6,149 +6,59 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
import type { PageServerLoad } from './$types';
const signUpDefaults = {
firstName: '',
lastName: '',
email: '',
username: '',
password: '',
confirm_password: '',
terms: true,
firstName: '',
lastName: '',
email: '',
username: '',
password: '',
confirm_password: '',
terms: true,
};
export const load: PageServerLoad = async (event) => {
const { locals } = event;
const { locals } = event;
const authedUser = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
// if (userFullyAuthenticated(user, session)) {
// const message = { type: 'success', message: 'You are already signed in' } as const;
// throw redirect('/', message, event);
// } else if (userNotFullyAuthenticated(user, session)) {
// try {
// await lucia.invalidateSession(locals.session!.id!);
// } catch (error) {
// console.log('Session already invalidated');
// }
// const sessionCookie = lucia.createBlankSessionCookie();
// cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
return {
signupForm: await superValidate(zod(signupUsernameEmailDto), {
defaults: signUpDefaults,
}),
};
return {
signupForm: await superValidate(zod(signupUsernameEmailDto), {
defaults: signUpDefaults,
}),
};
};
export const actions: Actions = {
default: async (event) => {
const { locals } = event;
default: async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const form = await superValidate(event, zod(signupUsernameEmailDto));
const form = await superValidate(event, zod(signupUsernameEmailDto));
const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse);
if (error) return setError(form, 'username', error);
if (!form.valid) {
const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse);
if (error) {
form.data.password = '';
form.data.confirm_password = '';
return fail(400, {
form,
});
}
return setError(form, 'username', 'Unable to log in.');
}
// let session;
// let sessionCookie;
// // Adding user to the db
// console.log('Check if user already exists');
//
// const existing_user = await db.query.usersTable.findFirst({
// where: eq(usersTable.username, form.data.username),
// });
//
// if (existing_user) {
// return setError(form, 'username', 'You cannot create an account with that username');
// }
//
// console.log('Creating user');
//
// const hashedPassword = await new Argon2id().hash(form.data.password);
//
// const user = await db
// .insert(usersTable)
// .values({
// username: form.data.username,
// hashed_password: hashedPassword,
// email: form.data.email,
// first_name: form.data.firstName ?? '',
// last_name: form.data.lastName ?? '',
// verified: false,
// receive_email: false,
// theme: 'system',
// })
// .returning();
// console.log('signup user', user);
//
// if (!user || user.length === 0) {
// return fail(400, {
// form,
// message: `Could not create your account. Please try again. If the problem persists, please contact support. Error ID: ${cuid2()}`,
// });
// }
//
// await add_user_to_role(user[0].id, 'user', true);
// await db.insert(collections).values({
// user_id: user[0].id,
// });
// await db.insert(wishlistsTable).values({
// user_id: user[0].id,
// });
//
// try {
// session = await lucia.createSession(user[0].id, {
// ip_country: event.locals.ip,
// ip_address: event.locals.country,
// twoFactorAuthEnabled: false,
// isTwoFactorAuthenticated: false,
// });
// sessionCookie = lucia.createSessionCookie(session.id);
// } catch (e: any) {
// if (e.message.toUpperCase() === `DUPLICATE_KEY_ID`) {
// // key already exists
// console.error('Lucia Error: ', e);
// }
// console.log(e);
// const message = {
// type: 'error',
// message: 'Unable to create your account. Please try again.',
// };
// form.data.password = '';
// form.data.confirm_password = '';
// error(500, message);
// }
//
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
if (!form.valid) {
form.data.password = '';
form.data.confirm_password = '';
return fail(400, {
form,
});
}
redirect(302, '/');
// const message = { type: 'success', message: 'Signed Up!' } as const;
// throw flashRedirect(message, event);
},
redirect(302, '/');
},
};

View file

@ -13,19 +13,16 @@ import type { PageServerLoad, RequestEvent } from './$types';
export const load: PageServerLoad = async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
const { session } = await locals.getAuthedUser();
if (session === null) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const { data } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (!data) {
if (!session?.twoFactorEnabled) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const { totpCredential } = data;
if (!totpCredential) {
throw redirect(302, '/login', notSignedInMessage, event);
if (session.twoFactorVerified) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
return {
@ -38,10 +35,14 @@ export const actions: Actions = {
validateTotp: async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
const { session } = await locals.getAuthedUser();
if (session === null) {
throw redirect(302, '/login', notSignedInMessage, event);
}
if (!session.twoFactorEnabled || session.twoFactorVerified) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const { data: totpData } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (!totpData) {
@ -64,16 +65,24 @@ export const actions: Actions = {
return setError(totpForm, 'code', totpVerifyError);
}
console.log('Successfully logged in');
return message(totpForm, { type: 'success', message: 'Successfully logged in!' });
totpForm.data.code = '';
const message = { type: 'success', message: 'Successfully logged in!' } as const;
redirect(302, '/', message, event);
},
validateRecoveryCode: async (event) => {
const { cookies, locals } = event;
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
const { session } = await locals.getAuthedUser();
if (session === null) {
throw redirect(302, '/login', notSignedInMessage, event);
}
if (!session?.twoFactorEnabled) {
throw redirect(302, '/login', notSignedInMessage, event);
}
if (session.twoFactorVerified) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const { dbUser, twoFactorDetails } = await validateUserData(event, locals);
@ -157,7 +166,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) {
throw fail(401);
}
const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated;
const twoFactorVerified = session?.twoFactorVerified;
const twoFactorDetails = await db.query.twoFactorTable.findFirst({
where: eq(twoFactorTable.userId, dbUser!.id!),
});
@ -167,7 +176,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) {
throw redirect(302, '/login', message, event);
}
if (isTwoFactorAuthenticated && twoFactorDetails.enabled && twoFactorDetails.secret !== '') {
if (twoFactorVerified && twoFactorDetails.enabled && twoFactorDetails.secret !== '') {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}

View file

@ -1,9 +1,11 @@
import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutServerLoad = loadFlash(async (event) => {
const { locals, url } = event;
const user = await locals.getAuthedUser();
const { user } = await locals.getAuthedUser();
return {
url: url.pathname,
user,