Moving around settings and security, fixing left nav to static list, fixing recovery codes generation.

This commit is contained in:
Bradley Shellnut 2024-09-06 17:35:16 -07:00
parent af59ee1cfd
commit 2eee00a20d
61 changed files with 902 additions and 668 deletions

View file

@ -1,14 +1,14 @@
{ {
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default", "style": "default",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/app.postcss", "css": "src/lib/styles/app.pcss",
"baseColor": "slate" "baseColor": "slate"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils" "utils": "$lib/utils/ui"
}, },
"typescript": true "typescript": true
} }

View file

@ -1,6 +1,6 @@
import 'dotenv/config' import 'dotenv/config'
import env from '$lib/server/api/common/env'
import { defineConfig } from 'drizzle-kit' import { defineConfig } from 'drizzle-kit'
import env from './src/env'
export default defineConfig({ export default defineConfig({
dialect: 'postgresql', dialect: 'postgresql',

View file

@ -27,7 +27,7 @@
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0", "@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.47.0",
"@sveltejs/adapter-auto": "^3.2.4", "@sveltejs/adapter-auto": "^3.2.4",
"@sveltejs/enhanced-img": "^0.3.4", "@sveltejs/enhanced-img": "^0.3.4",
"@sveltejs/kit": "^2.5.26", "@sveltejs/kit": "^2.5.26",
@ -41,7 +41,7 @@
"drizzle-kit": "^0.23.2", "drizzle-kit": "^0.23.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0", "eslint-plugin-svelte": "2.36.0-next.13",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"lucia": "3.2.0", "lucia": "3.2.0",
@ -95,7 +95,7 @@
"arctic": "^1.9.2", "arctic": "^1.9.2",
"bits-ui": "^0.21.13", "bits-ui": "^0.21.13",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.12.13", "bullmq": "^5.12.14",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^0.6.0", "cookie": "^0.6.0",

View file

@ -66,8 +66,8 @@ importers:
specifier: ^1.9.1 specifier: ^1.9.1
version: 1.9.1 version: 1.9.1
bullmq: bullmq:
specifier: ^5.12.13 specifier: ^5.12.14
version: 5.12.13 version: 5.12.14
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
@ -181,8 +181,8 @@ importers:
specifier: ^0.83.0 specifier: ^0.83.0
version: 0.83.0(svelte@5.0.0-next.175) version: 0.83.0(svelte@5.0.0-next.175)
'@playwright/test': '@playwright/test':
specifier: ^1.46.1 specifier: ^1.47.0
version: 1.46.1 version: 1.47.0
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(@sveltejs/kit@2.5.26(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)))(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0))) version: 3.2.4(@sveltejs/kit@2.5.26(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)))(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)))
@ -223,8 +223,8 @@ importers:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@8.57.0) version: 9.1.0(eslint@8.57.0)
eslint-plugin-svelte: eslint-plugin-svelte:
specifier: ^2.43.0 specifier: 2.36.0-next.13
version: 2.43.0(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)) version: 2.36.0-next.13(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4))
just-clone: just-clone:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0 version: 6.2.0
@ -1706,8 +1706,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@playwright/test@1.46.1': '@playwright/test@1.47.0':
resolution: {integrity: sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==} resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@ -2295,8 +2295,8 @@ packages:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'} engines: {node: '>=6'}
bullmq@5.12.13: bullmq@5.12.14:
resolution: {integrity: sha512-bFk0s1U9eQ8vKrhH9zYg/1H0+puSLVXuuq/pIW2jxgUmtLebRUBZr0cHJx35azTf2oPUJ+xXfpfHWaUtm4ZveA==} resolution: {integrity: sha512-mcSQHq9EY+DKtAP6XSmkP+0f1ifFithcpLTwo8WmUauArE9dxk45Gae3Fls1Nwf0Er9MoaDhPcglfe6LV/XCOg==}
bytes@3.1.2: bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
@ -2769,12 +2769,12 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=7.0.0' eslint: '>=7.0.0'
eslint-plugin-svelte@2.43.0: eslint-plugin-svelte@2.36.0-next.13:
resolution: {integrity: sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==} resolution: {integrity: sha512-N4bLGdFkGbbAQiKvX17kLfBgnZ+Em00khOY3AReppO7fkP9jaSxwjdgTCcWf+Q5/uZWor58g4GleRqHcb2Dk2w==}
engines: {node: ^14.17.0 || >=16.0.0} engines: {node: ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0
svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.191 svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.73
peerDependenciesMeta: peerDependenciesMeta:
svelte: svelte:
optional: true optional: true
@ -3212,8 +3212,8 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
known-css-properties@0.34.0: known-css-properties@0.30.0:
resolution: {integrity: sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==} resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==}
levn@0.4.1: levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
@ -3673,13 +3673,13 @@ packages:
pkg-types@1.2.0: pkg-types@1.2.0:
resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==}
playwright-core@1.46.1: playwright-core@1.47.0:
resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
playwright@1.46.1: playwright@1.47.0:
resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@ -5888,9 +5888,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
'@playwright/test@1.46.1': '@playwright/test@1.47.0':
dependencies: dependencies:
playwright: 1.46.1 playwright: 1.47.0
'@polka/url@1.0.0-next.25': {} '@polka/url@1.0.0-next.25': {}
@ -6508,7 +6508,7 @@ snapshots:
builtin-modules@3.3.0: {} builtin-modules@3.3.0: {}
bullmq@5.12.13: bullmq@5.12.14:
dependencies: dependencies:
cron-parser: 4.9.0 cron-parser: 4.9.0
ioredis: 5.4.1 ioredis: 5.4.1
@ -6936,14 +6936,15 @@ snapshots:
dependencies: dependencies:
eslint: 8.57.0 eslint: 8.57.0
eslint-plugin-svelte@2.43.0(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)): eslint-plugin-svelte@2.36.0-next.13(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
debug: 4.3.6
eslint: 8.57.0 eslint: 8.57.0
eslint-compat-utils: 0.5.1(eslint@8.57.0) eslint-compat-utils: 0.5.1(eslint@8.57.0)
esutils: 2.0.3 esutils: 2.0.3
known-css-properties: 0.34.0 known-css-properties: 0.30.0
postcss: 8.4.45 postcss: 8.4.45
postcss-load-config: 3.1.4(postcss@8.4.45)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)) postcss-load-config: 3.1.4(postcss@8.4.45)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4))
postcss-safe-parser: 6.0.0(postcss@8.4.45) postcss-safe-parser: 6.0.0(postcss@8.4.45)
@ -6953,6 +6954,7 @@ snapshots:
optionalDependencies: optionalDependencies:
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
transitivePeerDependencies: transitivePeerDependencies:
- supports-color
- ts-node - ts-node
eslint-scope@7.2.2: eslint-scope@7.2.2:
@ -7479,7 +7481,7 @@ snapshots:
kleur@4.1.5: {} kleur@4.1.5: {}
known-css-properties@0.34.0: {} known-css-properties@0.30.0: {}
levn@0.4.1: levn@0.4.1:
dependencies: dependencies:
@ -7882,11 +7884,11 @@ snapshots:
mlly: 1.7.1 mlly: 1.7.1
pathe: 1.1.2 pathe: 1.1.2
playwright-core@1.46.1: {} playwright-core@1.47.0: {}
playwright@1.46.1: playwright@1.47.0:
dependencies: dependencies:
playwright-core: 1.46.1 playwright-core: 1.47.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2

View file

@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms'
import toast from 'svelte-french-toast'; import { invalidateAll } from '$app/navigation'
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'; import Logo from '$components/logo.svelte'
import * as DropdownMenu from '$components/ui/dropdown-menu'; import * as Avatar from '$components/ui/avatar'
import * as Avatar from '$components/ui/avatar'; import * as DropdownMenu from '$components/ui/dropdown-menu'
import { invalidateAll } from '$app/navigation'; import type { Users } from '$db/schema'
import Logo from '$components/logo.svelte'; import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'
import type { Users } from '$db/schema'; import toast from 'svelte-french-toast'
type HeaderProps = { type HeaderProps = {
user: Users | null; user: Users | null
}; }
let { user = null }: HeaderProps = $props(); let { user = null }: HeaderProps = $props()
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)'); let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
</script> </script>
<header> <header>
@ -40,10 +40,10 @@
<DropdownMenu.Group> <DropdownMenu.Group>
<DropdownMenu.Label>Account</DropdownMenu.Label> <DropdownMenu.Label>Account</DropdownMenu.Label>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<a href="/profile"> <a href="/settings">
<DropdownMenu.Item> <DropdownMenu.Item>
<User class="mr-2 h-4 w-4" /> <User class="mr-2 h-4 w-4" />
<span>Profile</span> <span>Settings</span>
</DropdownMenu.Item> </DropdownMenu.Item>
</a> </a>
<a href="/collections"> <a href="/collections">

View file

@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores' import { page } from '$app/stores'
import type { Route } from '$lib/types'
type Route = {
href: string
label: string
}
let { children, routes }: { children: unknown; routes: Route[] } = $props() let { children, routes }: { children: unknown; routes: Route[] } = $props()
</script> </script>
<div class="mx-auto grid w-full max-w-6xl gap-2">
<h1 class="text-3xl font-semibold">Settings</h1>
</div>
<div class="security-nav"> <div class="security-nav">
<nav> <nav>
<ul> <ul>
{#each routes as { href, label }} {#each routes as { href, label }}
<li> <li>
<a href={href} class:active={$page.url.pathname === href}> <a href={href} class:active={$page.url.pathname.includes(href)}>
{label} {label}
</a> </a>
</li> </li>
@ -62,8 +62,8 @@ let { children, routes }: { children: unknown; routes: Route[] } = $props()
} }
&.active { &.active {
color: #23527c; color: var(--color-link-hover);
font-weight: bold; font-weight: 600;
background-color: #e9ecef; background-color: #e9ecef;
} }
} }

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils"; import { type Variant, badgeVariants } from "./index.js";
import { badgeVariants, type Variant } from "."; import { cn } from "$lib/utils/ui.js";
let className: string | undefined | null = undefined; let className: string | undefined | null = undefined;
export let href: string | undefined = undefined; export let href: string | undefined = undefined;

View file

@ -1,22 +1,21 @@
import { tv, type VariantProps } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export { default as Badge } from "./badge.svelte"; export { default as Badge } from "./badge.svelte";
export const badgeVariants = tv({ export const badgeVariants = tv({
base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2", base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
secondary: secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive: destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground" outline: "text-foreground",
} },
}, },
defaultVariants: { defaultVariants: {
variant: "default" variant: "default",
} },
}); });
export type Variant = VariantProps<typeof badgeVariants>["variant"]; export type Variant = VariantProps<typeof badgeVariants>["variant"];

View file

@ -0,0 +1,38 @@
import env from './env'
import type { Config } from './types/config'
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development'
let domain: string
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
domain = 'boredgame.vercel.app'
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
domain = process.env.VERCEL_BRANCH_URL
} else {
domain = 'localhost'
}
// export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
// || process.env.VERCEL_ENV === 'production', domain };
export const config: Config = {
isProduction: process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production',
domain,
api: {
origin: env.ORIGIN,
},
redis: {
url: env.REDIS_URL,
},
postgres: {
user: env.DATABASE_USER,
password: env.DATABASE_PASSWORD,
host: env.DATABASE_HOST,
port: env.DATABASE_PORT,
database: env.DATABASE_DB,
ssl: env.DATABASE_HOST !== 'localhost',
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
},
}
console.log('config', config)

View file

@ -10,8 +10,6 @@ const stringBoolean = z.coerce
.default('false') .default('false')
const EnvSchema = z.object({ const EnvSchema = z.object({
ADMIN_USERNAME: z.string(),
ADMIN_PASSWORD: z.string(),
DATABASE_USER: z.string(), DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(), DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(), DATABASE_HOST: z.string(),

View file

@ -1,7 +1,8 @@
export interface Config { export interface Config {
isProduction: boolean isProduction: boolean
domain: string
api: ApiConfig api: ApiConfig
storage: StorageConfig // storage: StorageConfig
redis: RedisConfig redis: RedisConfig
postgres: PostgresConfig postgres: PostgresConfig
} }
@ -10,17 +11,23 @@ interface ApiConfig {
origin: string origin: string
} }
interface StorageConfig { // interface StorageConfig {
accessKey: string // accessKey: string
secretKey: string // secretKey: string
bucket: string // bucket: string
url: string // url: string
} // }
interface RedisConfig { interface RedisConfig {
url: string url: string
} }
interface PostgresConfig { interface PostgresConfig {
url: string user: string
password: string
host: string
port: number
database: string
ssl: boolean
max: number | undefined
} }

View file

@ -1,15 +0,0 @@
import env from '../../../../env';
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development';
let domain: string;
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
domain = 'boredgame.vercel.app';
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
domain = process.env.VERCEL_BRANCH_URL;
} else {
domain = 'localhost';
}
export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
|| process.env.VERCEL_ENV === 'production', domain };

View file

@ -49,8 +49,9 @@ export class MfaController extends Controller {
const user = c.var.user const user = c.var.user
// You can only view recovery codes once and that is on creation // You can only view recovery codes once and that is on creation
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id) const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id)
if (existingCodes) { if (existingCodes && existingCodes.length > 0) {
return c.body('You have already generated recovery codes', StatusCodes.BAD_REQUEST) console.log('Recovery Codes found', existingCodes)
return c.json({ recoveryCodes: existingCodes })
} }
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id) const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
return c.json({ recoveryCodes }) return c.json({ recoveryCodes })

View file

@ -3,7 +3,7 @@ import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator' import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres' import postgres from 'postgres'
import config from '../../../../../drizzle.config' import config from '../../../../../drizzle.config'
import env from '../../../../env' import env from '../common/env'
const connection = postgres({ const connection = postgres({
host: env.DATABASE_HOST || 'localhost', host: env.DATABASE_HOST || 'localhost',

View file

@ -2,7 +2,7 @@ import 'reflect-metadata'
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type Table, getTableName, sql } from 'drizzle-orm' import { type Table, getTableName, sql } from 'drizzle-orm'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres' import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import env from '../../../../env' import env from '../common/env'
import * as seeds from './seeds' import * as seeds from './seeds'
import * as schema from './tables' import * as schema from './tables'

View file

@ -2,7 +2,7 @@ import * as schema from '$lib/server/api/databases/tables'
import type { db } from '$lib/server/api/packages/drizzle' import type { db } from '$lib/server/api/packages/drizzle'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { Argon2id } from 'oslo/password' import { Argon2id } from 'oslo/password'
import { config } from '../../configs/config' import { config } from '../../common/config'
import users from './data/users.json' import users from './data/users.json'
type JsonRole = { type JsonRole = {
@ -18,7 +18,7 @@ export default async function seed(db: db) {
const adminUser = await db const adminUser = await db
.insert(schema.usersTable) .insert(schema.usersTable)
.values({ .values({
username: `${config.ADMIN_USERNAME}`, username: `${process.env.ADMIN_USERNAME}`,
email: '', email: '',
first_name: 'Brad', first_name: 'Brad',
last_name: 'S', last_name: 'S',
@ -32,7 +32,7 @@ export default async function seed(db: db) {
await db.insert(schema.credentialsTable).values({ await db.insert(schema.credentialsTable).values({
user_id: adminUser[0].id, user_id: adminUser[0].id,
type: schema.CredentialsType.PASSWORD, type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`), secret_data: await new Argon2id().hash(`${process.env.ADMIN_PASSWORD}`),
}) })
await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing() await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing()

View file

@ -1,4 +1,4 @@
import { type InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'

View file

@ -10,7 +10,7 @@ import { hc } from 'hono/client'
import { cors } from 'hono/cors' import { cors } from 'hono/cors'
import { logger } from 'hono/logger' import { logger } from 'hono/logger'
import { container } from 'tsyringe' import { container } from 'tsyringe'
import { config } from './configs/config' import { config } from './common/config'
import { IamController } from './controllers/iam.controller' import { IamController } from './controllers/iam.controller'
import { LoginController } from './controllers/login.controller' import { LoginController } from './controllers/login.controller'
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware' import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'
@ -45,7 +45,7 @@ const routes = app
.route('/user', container.resolve(UserController).routes()) .route('/user', container.resolve(UserController).routes())
.route('/login', container.resolve(LoginController).routes()) .route('/login', container.resolve(LoginController).routes())
.route('/signup', container.resolve(SignupController).routes()) .route('/signup', container.resolve(SignupController).routes())
.route('/wishlistsTable', container.resolve(WishlistController).routes()) .route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes()) .route('/collections', container.resolve(CollectionController).routes())
.route('/mfa', container.resolve(MfaController).routes()) .route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' })) .get('/', (c) => c.json({ message: 'Server is healthy' }))

View file

@ -1,6 +1,6 @@
import { drizzle } from 'drizzle-orm/node-postgres' import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg' import pg from 'pg'
import { config } from '../configs/config' import { config } from '../common/config'
import * as schema from '../databases/tables' import * as schema from '../databases/tables'
// create the connection // create the connection

View file

@ -1,6 +1,6 @@
import RedisClient from 'ioredis' import RedisClient from 'ioredis'
import { container } from 'tsyringe' import { container } from 'tsyringe'
import { config } from '../configs/config' import { config } from '../common/config'
export const RedisProvider = Symbol('REDIS_TOKEN') export const RedisProvider = Symbol('REDIS_TOKEN')
export type RedisProvider = RedisClient export type RedisProvider = RedisClient

View file

@ -1,4 +1,5 @@
import 'reflect-metadata' import 'reflect-metadata'
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository'
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type InferInsertModel, eq } from 'drizzle-orm' import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
@ -10,6 +11,10 @@ export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable>
export class RecoveryCodesRepository { export class RecoveryCodesRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {} constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async create(data: CreateRecoveryCodes, db = this.drizzle.db) {
return db.insert(recoveryCodesTable).values(data).returning().then(takeFirstOrThrow)
}
async findAllByUserId(userId: string, db = this.drizzle.db) { async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findFirst({ return db.query.recoveryCodesTable.findFirst({
where: eq(recoveryCodesTable.userId, userId), where: eq(recoveryCodesTable.userId, userId),

View file

@ -12,11 +12,11 @@ export class WishlistsRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {} constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async findAll(db = this.drizzle.db) { async findAll(db = this.drizzle.db) {
return db.query.wishlists.findMany() return db.query.wishlistsTable.findMany()
} }
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
return db.query.wishlists.findFirst({ return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.id, id), where: eq(wishlistsTable.id, id),
columns: { columns: {
cuid: true, cuid: true,
@ -26,7 +26,7 @@ export class WishlistsRepository {
} }
async findOneByCuid(cuid: string, db = this.drizzle.db) { async findOneByCuid(cuid: string, db = this.drizzle.db) {
return db.query.wishlists.findFirst({ return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.cuid, cuid), where: eq(wishlistsTable.cuid, cuid),
columns: { columns: {
cuid: true, cuid: true,
@ -36,7 +36,7 @@ export class WishlistsRepository {
} }
async findOneByUserId(userId: string, db = this.drizzle.db) { async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.wishlists.findFirst({ return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.user_id, userId), where: eq(wishlistsTable.user_id, userId),
columns: { columns: {
cuid: true, cuid: true,
@ -46,7 +46,7 @@ export class WishlistsRepository {
} }
async findAllByUserId(userId: string, db = this.drizzle.db) { async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.wishlists.findMany({ return db.query.wishlistsTable.findMany({
where: eq(wishlistsTable.user_id, userId), where: eq(wishlistsTable.user_id, userId),
columns: { columns: {
cuid: true, cuid: true,

View file

@ -1,4 +1,4 @@
import { config } from '$lib/server/api/configs/config' import { config } from '$lib/server/api/common/config'
import * as schema from '$lib/server/api/databases/tables' import * as schema from '$lib/server/api/databases/tables'
import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres' import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg' import pg from 'pg'
@ -12,18 +12,18 @@ export class DrizzleService implements Disposable {
constructor() { constructor() {
const pool = new pg.Pool({ const pool = new pg.Pool({
user: config.DATABASE_USER, user: config.postgres.user,
password: config.DATABASE_PASSWORD, password: config.postgres.password,
host: config.DATABASE_HOST, host: config.postgres.host,
port: Number(config.DATABASE_PORT).valueOf(), port: Number(config.postgres.port).valueOf(),
database: config.DATABASE_DB, database: config.postgres.database,
ssl: config.DATABASE_HOST !== 'localhost', ssl: config.postgres.ssl,
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined, max: config.postgres.max,
}) })
this.pool = pool this.pool = pool
this.db = drizzle(pool, { this.db = drizzle(pool, {
schema, schema,
logger: config.NODE_ENV === 'development', logger: process.env.NODE_ENV === 'development',
}) })
} }

View file

@ -1,4 +1,4 @@
import { config } from '$lib/server/api/configs/config' import { config } from '$lib/server/api/common/config'
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle' import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'
import { Lucia, TimeSpan } from 'lucia' import { Lucia, TimeSpan } from 'lucia'

View file

@ -1,6 +1,6 @@
import { injectable } from 'tsyringe' import { injectable } from 'tsyringe'
import { config } from '../common/config'
import type { Email } from '../common/inferfaces/email.interface' import type { Email } from '../common/inferfaces/email.interface'
import { config } from '../configs/config'
type SendProps = { type SendProps = {
to: string | string[] to: string | string[]

View file

@ -1,6 +1,4 @@
import 'reflect-metadata' import 'reflect-metadata'
import { recoveryCodesTable } from '$lib/server/api/databases/tables'
import { db } from '$lib/server/api/packages/drizzle'
import { RecoveryCodesRepository } from '$lib/server/api/repositories/recovery-codes.repository' import { RecoveryCodesRepository } from '$lib/server/api/repositories/recovery-codes.repository'
import { alphabet, generateRandomString } from 'oslo/crypto' import { alphabet, generateRandomString } from 'oslo/crypto'
import { Argon2id } from 'oslo/password' import { Argon2id } from 'oslo/password'
@ -20,10 +18,7 @@ export class RecoveryCodesService {
for (const code of createdRecoveryCodes) { for (const code of createdRecoveryCodes) {
const hashedCode = await new Argon2id().hash(code) const hashedCode = await new Argon2id().hash(code)
console.log('Inserting recovery code', code, hashedCode) console.log('Inserting recovery code', code, hashedCode)
await db.insert(recoveryCodesTable).values({ await this.recoveryCodesRepository.create({ userId, code: hashedCode })
userId,
code: hashedCode,
})
} }
return createdRecoveryCodes return createdRecoveryCodes

View file

@ -1,4 +1,4 @@
import { config } from '$lib/server/api/configs/config' import { config } from '$lib/server/api/common/config'
import { Redis } from 'ioredis' import { Redis } from 'ioredis'
import { type Disposable, injectable } from 'tsyringe' import { type Disposable, injectable } from 'tsyringe'

View file

@ -1,31 +1,36 @@
import type { SvelteComponent } from 'svelte'; import type { collections } from '$lib/server/api/databases/tables'
import { collections } from '$db/schema'; import type { SvelteComponent } from 'svelte'
export type Message = { status: 'error' | 'success' | 'warning' | 'info'; text: string }; export type Message = { status: 'error' | 'success' | 'warning' | 'info'; text: string }
export type Route = {
href: string
label: string
}
export type Dialog = { export type Dialog = {
isOpen: boolean; isOpen: boolean
content?: typeof SvelteComponent<any>; content?: typeof SvelteComponent<any>
additionalData?: SavedGameType | GameType; additionalData?: SavedGameType | GameType
}; }
export type Search = { export type Search = {
name: string; name: string
minAge: string; minAge: string
minPlayers: string; minPlayers: string
maxPlayers: string; maxPlayers: string
exactMinAge: string; exactMinAge: string
exactMinPlayers: string; exactMinPlayers: string
exactMaxPlayers: string; exactMaxPlayers: string
skip: number; skip: number
currentPage: number; currentPage: number
limit: number; limit: number
}; }
export type BoredStore = { export type BoredStore = {
loading: boolean; loading: boolean
dialog: Dialog; dialog: Dialog
}; }
export enum ToastType { export enum ToastType {
INFO = 'INFO', INFO = 'INFO',
@ -34,146 +39,141 @@ export enum ToastType {
} }
export type ToastData = { export type ToastData = {
id: number; id: number
duration: number; duration: number
dismissible: boolean; dismissible: boolean
showButton: boolean; showButton: boolean
autoDismiss: boolean; autoDismiss: boolean
type: ToastType; type: ToastType
message: string; message: string
}; }
export type GameMechanic = { export type GameMechanic = {
id: string; id: string
name: string; name: string
boardGameAtlasLink: string; boardGameAtlasLink: string
}; }
export type SavedGameType = { export type SavedGameType = {
id: string; id: string
name: string; name: string
thumb_url: string; thumb_url: string
players: string; players: string
playtime: string; playtime: string
mechanics: GameMechanic[]; mechanics: GameMechanic[]
searchTerms: string; searchTerms: string
includeInRandom: boolean; includeInRandom: boolean
}; }
export type ListGameType = { export type ListGameType = {
id: string; id: string
game_id: string; game_id: string
collection_id: string | undefined; collection_id: string | undefined
wishlist_id: string | undefined; wishlist_id: string | undefined
times_played: number; times_played: number
thumb_url: string; thumb_url: string
}; }
export type MechanicType = { export type MechanicType = {
id: string; id: string
}; }
export type CategoryType = { export type CategoryType = {
id: string; id: string
}; }
export type PublisherType = { export type PublisherType = {
id: string; id: string
}; }
export type DesignerType = { export type DesignerType = {
id: string; id: string
}; }
export type ArtistType = { export type ArtistType = {
id: string; id: string
}; }
export type ExpansionType = { export type ExpansionType = {
id: string; id: string
}; }
export type BGGLinkType = export type BGGLinkType = 'boardgamecategory' | 'boardgamemechanic' | 'boardgameexpansion' | 'boardgameartist' | 'boardgamepublisher'
| 'boardgamecategory'
| 'boardgamemechanic'
| 'boardgameexpansion'
| 'boardgameartist'
| 'boardgamepublisher';
export type BGGLink = { export type BGGLink = {
id: number; id: number
type: BGGLinkType; type: BGGLinkType
value: string; value: string
}; }
export type GameType = { export type GameType = {
id: string; id: string
name: string; name: string
slug: string; slug: string
url: string; url: string
edit_url: string; edit_url: string
thumb_url: string; thumb_url: string
image_url: string; image_url: string
price: number; price: number
price_ca: number; price_ca: number
price_uk: number; price_uk: number
price_au: number; price_au: number
msrp: number; msrp: number
year_published: number; year_published: number
categories: CategoryType[]; categories: CategoryType[]
mechanics: MechanicType[]; mechanics: MechanicType[]
primary_publisher: PublisherType; primary_publisher: PublisherType
publishers: PublisherType[]; publishers: PublisherType[]
primary_designer: DesignerType; primary_designer: DesignerType
designers: DesignerType[]; designers: DesignerType[]
developers: String[]; developers: string[]
artists: ArtistType[]; artists: ArtistType[]
expansions: ExpansionType[]; expansions: ExpansionType[]
min_players: number; min_players: number
max_players: number; max_players: number
min_playtime: number; min_playtime: number
max_playtime: number; max_playtime: number
min_age: number; min_age: number
description: string; description: string
players: string; players: string
playtime: number; playtime: number
external_id: number; external_id: number
}; }
export type SearchQuery = { export type SearchQuery = {
limit?: number; limit?: number
skip?: number; skip?: number
ids?: string[]; ids?: string[]
list_id?: string; list_id?: string
random?: boolean; random?: boolean
q?: string; q?: string
exact?: boolean; exact?: boolean
designer?: string; designer?: string
publisher?: string; publisher?: string
artist?: string; artist?: string
mechanics?: string; mechanics?: string
categories?: string; categories?: string
order_by?: string; order_by?: string
ascending?: boolean; ascending?: boolean
min_players?: number; min_players?: number
max_players?: number; max_players?: number
min_playtime?: number; min_playtime?: number
max_playtime?: number; max_playtime?: number
min_age?: number; min_age?: number
year_published?: number; year_published?: number
gt_min_players?: number; gt_min_players?: number
gt_max_players?: number; gt_max_players?: number
gt_min_playtime?: number; gt_min_playtime?: number
gt_max_playtime?: number; gt_max_playtime?: number
gt_min_age?: number; gt_min_age?: number
gt_year_published?: number; gt_year_published?: number
lt_min_players?: number; lt_min_players?: number
lt_max_players?: number; lt_max_players?: number
lt_min_playtime?: number; lt_min_playtime?: number
lt_max_playtime?: number; lt_max_playtime?: number
lt_min_age?: number; lt_min_age?: number
lt_year_published?: number; lt_year_published?: number
fields?: string; fields?: string
}; }
export type UICollection = Pick<typeof collections, 'cuid' | 'name'>; export type UICollection = Pick<typeof collections, 'cuid' | 'name'>

View file

@ -1,56 +1,53 @@
import { type ClassValue, clsx } from 'clsx'; import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'; import { cubicOut } from 'svelte/easing'
import { cubicOut } from 'svelte/easing'; import type { TransitionConfig } from 'svelte/transition'
import type { TransitionConfig } from 'svelte/transition'; import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }
type FlyAndScaleParams = { type FlyAndScaleParams = {
y?: number; y?: number
x?: number; x?: number
start?: number; start?: number
duration?: number; duration?: number
}; }
export const flyAndScale = ( export const flyAndScale = (node: Element, params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }): TransitionConfig => {
node: Element, const style = getComputedStyle(node)
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }, const transform = style.transform === 'none' ? '' : style.transform
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA; const [minA, maxA] = scaleA
const [minB, maxB] = scaleB; const [minB, maxB] = scaleB
const percentage = (valueA - minA) / (maxA - minA); const percentage = (valueA - minA) / (maxA - minA)
const valueB = percentage * (maxB - minB) + minB; const valueB = percentage * (maxB - minB) + minB
return valueB; return valueB
}; }
const styleToString = (style: Record<string, number | string | undefined>): string => { const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => { return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str; if (style[key] === undefined) return str
return str + `${key}:${style[key]};`; return str + `${key}:${style[key]};`
}, ''); }, '')
}; }
return { return {
duration: params.duration ?? 200, duration: params.duration ?? 200,
delay: 0, delay: 0,
css: (t) => { css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0])
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0])
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1])
return styleToString({ return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t, opacity: t,
}); })
}, },
easing: cubicOut, easing: cubicOut,
}; }
}; }

View file

@ -1,112 +1,69 @@
import { z } from 'zod'; import { z } from 'zod'
import { userSchema } from './zod-schemas'; import { userSchema } from './zod-schemas'
export const profileSchema = userSchema.pick({
firstName: true,
lastName: true,
username: true,
});
export const changeEmailSchema = userSchema.pick({
email: true,
});
export const changeUserPasswordSchema = z
.object({
current_password: z.string({ required_error: 'Current Password is required' }),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
export const addTwoFactorSchema = z.object({
current_password: z.string({ required_error: 'Current Password is required' }),
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
});
export type AddTwoFactorSchema = typeof addTwoFactorSchema;
export const removeTwoFactorSchema = addTwoFactorSchema.pick({
current_password: true,
});
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema;
export const updateUserPasswordSchema = userSchema export const updateUserPasswordSchema = userSchema
.pick({ password: true, confirm_password: true }) .pick({ password: true, confirm_password: true })
.superRefine(({ confirm_password, password }, ctx) => { .superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx); refinePasswords(confirm_password, password, ctx)
}); })
export const refinePasswords = async function ( export const refinePasswords = async function (confirm_password: string, password: string, ctx: z.RefinementCtx) {
confirm_password: string, comparePasswords(confirm_password, password, ctx)
password: string, checkPasswordStrength(password, ctx)
ctx: z.RefinementCtx, }
) {
comparePasswords(confirm_password, password, ctx);
checkPasswordStrength(password, ctx);
};
const comparePasswords = async function ( const comparePasswords = async function (confirm_password: string, password: string, ctx: z.RefinementCtx) {
confirm_password: string,
password: string,
ctx: z.RefinementCtx,
) {
if (confirm_password !== password) { if (confirm_password !== password) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Password and Confirm Password must match', message: 'Password and Confirm Password must match',
path: ['confirm_password'], path: ['confirm_password'],
}); })
} }
}; }
const checkPasswordStrength = async function (password: string, ctx: z.RefinementCtx) { const checkPasswordStrength = async function (password: string, ctx: z.RefinementCtx) {
const minimumLength = password.length < 8; const minimumLength = password.length < 8
const maximumLength = password.length > 128; const maximumLength = password.length > 128
const containsUppercase = (ch: string) => /[A-Z]/.test(ch); const containsUppercase = (ch: string) => /[A-Z]/.test(ch)
const containsLowercase = (ch: string) => /[a-z]/.test(ch); const containsLowercase = (ch: string) => /[a-z]/.test(ch)
const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch); const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch)
let countOfUpperCase = 0, let countOfUpperCase = 0,
countOfLowerCase = 0, countOfLowerCase = 0,
countOfNumbers = 0, countOfNumbers = 0,
countOfSpecialChar = 0; countOfSpecialChar = 0
for (let i = 0; i < password.length; i++) { for (let i = 0; i < password.length; i++) {
const char = password.charAt(i); const char = password.charAt(i)
if (!isNaN(+char)) { if (!isNaN(+char)) {
countOfNumbers++; countOfNumbers++
} else if (containsUppercase(char)) { } else if (containsUppercase(char)) {
countOfUpperCase++; countOfUpperCase++
} else if (containsLowercase(char)) { } else if (containsLowercase(char)) {
countOfLowerCase++; countOfLowerCase++
} else if (containsSpecialChar(char)) { } else if (containsSpecialChar(char)) {
countOfSpecialChar++; countOfSpecialChar++
} }
} }
let errorMessage = 'Your password:'; let errorMessage = 'Your password:'
if (countOfLowerCase < 1) { if (countOfLowerCase < 1) {
errorMessage = ' Must have at least one lowercase letter. '; errorMessage = ' Must have at least one lowercase letter. '
} }
if (countOfNumbers < 1) { if (countOfNumbers < 1) {
errorMessage += ' Must have at least one number. '; errorMessage += ' Must have at least one number. '
} }
if (countOfUpperCase < 1) { if (countOfUpperCase < 1) {
errorMessage += ' Must have at least one uppercase letter. '; errorMessage += ' Must have at least one uppercase letter. '
} }
if (countOfSpecialChar < 1) { if (countOfSpecialChar < 1) {
errorMessage += ' Must have at least one special character.'; errorMessage += ' Must have at least one special character.'
} }
if (minimumLength) { if (minimumLength) {
errorMessage += ' Be at least 8 characters long.'; errorMessage += ' Be at least 8 characters long.'
} }
if (maximumLength) { if (maximumLength) {
errorMessage += ' Be less than 128 characters long.'; errorMessage += ' Be less than 128 characters long.'
} }
if (errorMessage.length > 'Your password:'.length) { if (errorMessage.length > 'Your password:'.length) {
@ -114,14 +71,14 @@ const checkPasswordStrength = async function (password: string, ctx: z.Refinemen
code: 'custom', code: 'custom',
message: errorMessage, message: errorMessage,
path: ['password'], path: ['password'],
}); })
} }
}; }
export const addRoleSchema = z.object({ export const addRoleSchema = z.object({
roles: z.array(z.string()).refine((value) => value.some((item) => item), { roles: z.array(z.string()).refine((value) => value.some((item) => item), {
message: 'You have to select at least one item.', message: 'You have to select at least one item.',
}), }),
}); })
export type AddRoleSchema = typeof addRoleSchema; export type AddRoleSchema = typeof addRoleSchema

View file

@ -10,9 +10,11 @@ import { superValidate } from 'sveltekit-superforms/server'
import { collection_items, collections, gamesTable } from '../../../../lib/server/api/databases/tables' import { collection_items, collections, gamesTable } from '../../../../lib/server/api/databases/tables'
export async function load(event) { export async function load(event) {
const { user, session } = event.locals const { locals } = event
if (userNotAuthenticated(user, session)) {
redirect(302, '/login', notSignedInMessage, event) const authedUser = await locals.getAuthedUser()
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
try { try {
@ -22,7 +24,7 @@ export async function load(event) {
name: true, name: true,
created_at: true, created_at: true,
}, },
where: eq(collections.user_id, user!.id!), where: eq(collections.user_id, authedUser.id),
}) })
console.log('collections', userCollections) console.log('collections', userCollections)
@ -46,14 +48,17 @@ export async function load(event) {
export const actions: Actions = { export const actions: Actions = {
// Add game to a wishlist // Add game to a wishlist
add: async (event) => { add: async (event) => {
const form = await superValidate(event, zod(modifyListGameSchema)) const { locals } = event
if (!event.locals.user) { const authedUser = await locals.getAuthedUser()
throw fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema))
const user = event.locals.user const user = event.locals.user
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -108,13 +113,15 @@ export const actions: Actions = {
// Remove game from a wishlist // Remove game from a wishlist
remove: async (event) => { remove: async (event) => {
const { locals } = event const { locals } = event
const form = await superValidate(event, zod(modifyListGameSchema))
if (!locals.user) { const authedUser = await locals.getAuthedUser()
throw fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const game = await db.query.games.findFirst({ const form = await superValidate(event, zod(modifyListGameSchema))
const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -125,7 +132,7 @@ export const actions: Actions = {
try { try {
const collection = await db.query.collections.findFirst({ const collection = await db.query.collections.findFirst({
where: eq(collections.user_id, locals.user.id), where: eq(collections.user_id, authedUser.id),
}) })
if (!collection) { if (!collection) {

View file

@ -131,14 +131,15 @@ export const actions: Actions = {
// Add game to a wishlist // Add game to a wishlist
add: async (event) => { add: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -154,7 +155,7 @@ export const actions: Actions = {
try { try {
const collection = await db.query.collections.findFirst({ const collection = await db.query.collections.findFirst({
where: eq(collections.user_id, user!.id!), where: eq(collections.user_id, authedUser.id),
}) })
if (!collection) { if (!collection) {
@ -179,31 +180,35 @@ export const actions: Actions = {
// Create new wishlist // Create new wishlist
create: async (event) => { create: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
// Delete a wishlist // Delete a wishlist
delete: async (event) => { delete: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
// Remove game from a wishlist // Remove game from a wishlist
remove: async (event) => { remove: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -214,7 +219,7 @@ export const actions: Actions = {
try { try {
const collection = await db.query.collections.findFirst({ const collection = await db.query.collections.findFirst({
where: eq(collections.user_id, user!.id!), where: eq(collections.user_id, authedUser.id),
}) })
if (!collection) { if (!collection) {

View file

@ -1,13 +1,14 @@
import { redirect } from 'sveltekit-flash-message/server'; import { notSignedInMessage } from '$lib/flashMessages'
import { notSignedInMessage } from '$lib/flashMessages'; import { userNotAuthenticated } from '$lib/server/auth-utils'
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { redirect } from 'sveltekit-flash-message/server'
export async function load(event) { export async function load(event) {
const { locals } = event; const { locals } = event
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return {}; return {}
} }

View file

@ -1,19 +1,20 @@
import { redirect } from '@sveltejs/kit'; import { notSignedInMessage } from '$lib/flashMessages'
import { superValidate } from 'sveltekit-superforms/server'; import { userNotAuthenticated } from '$lib/server/auth-utils'
import { zod } from 'sveltekit-superforms/adapters'; import { BggForm } from '$lib/zodValidation'
import type { PageServerLoad } from '../$types'; import { redirect } from '@sveltejs/kit'
import { BggForm } from '$lib/zodValidation'; import { zod } from 'sveltekit-superforms/adapters'
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { superValidate } from 'sveltekit-superforms/server'
import { notSignedInMessage } from '$lib/flashMessages'; import type { PageServerLoad } from '../$types'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event
const { user, session } = locals;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate({}, zod(BggForm)); const form = await superValidate({}, zod(BggForm))
return { form }; return { form }
}; }

View file

@ -44,10 +44,11 @@ export async function load(event) {
export const actions: Actions = { export const actions: Actions = {
// Add game to a wishlist // Add game to a wishlist
add: async (event) => { add: async (event) => {
const { params, locals } = event const { locals, params } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
@ -61,7 +62,7 @@ export const actions: Actions = {
}) })
} }
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.id), where: eq(gamesTable.id, form.id),
}) })
@ -71,7 +72,7 @@ export const actions: Actions = {
}) })
} }
const wishlist = await db.query.wishlists.findFirst({ const wishlist = await db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.id, params.id), where: eq(wishlistsTable.id, params.id),
}) })
@ -103,25 +104,28 @@ export const actions: Actions = {
// Create new wishlist // Create new wishlist
create: async (event) => { create: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
}, },
// Delete a wishlist // Delete a wishlist
delete: async (event) => { delete: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
}, },
// Remove game from a wishlist // Remove game from a wishlist
remove: async (event) => { remove: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
}, },
} }

View file

@ -1,3 +0,0 @@
export const load = async () => {
return {}
}

View file

@ -1,15 +0,0 @@
<script lang="ts">
import LeftNav from '$components/LeftNav.svelte'
let { children } = $props()
const routes = [
{ href: '/profile/security', label: 'Security' },
{ href: '/profile/security/mfa', label: 'MFA' },
{ href: '/profile/security/password/change', label: 'Change Password' },
]
</script>
<LeftNav {routes}>
{@render children()}
</LeftNav>

View file

@ -1,83 +0,0 @@
<script lang="ts">
import PinInput from '$components/pin-input.svelte'
import * as Alert from '$components/ui/alert'
import * as Form from '$components/ui/form'
import { Input } from '$components/ui/input'
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account'
import { AlertTriangle } from 'lucide-svelte'
import { zodClient } from 'sveltekit-superforms/adapters'
import { superForm } from 'sveltekit-superforms/client'
export let data
const { qrCode, secret, twoFactorEnabled, recoveryCodes } = data
const addTwoFactorForm = superForm(data.addTwoFactorForm, {
taintedMessage: null,
validators: zodClient(addTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
})
const removeTwoFactorForm = superForm(data.removeTwoFactorForm, {
taintedMessage: null,
validators: zodClient(removeTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
})
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes)
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm
</script>
<section class="two-factor">
<h1>Two-Factor Authentication</h1>
{#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="?/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>
<Input type="password" {...attrs} bind:value={$removeTwoFactorFormData.current_password} />
</Form.Control>
<Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Disable Two Factor Authentication</Form.Button>
</form>
{:else}
<h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" />
<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>
<PinInput {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
</Form.Control>
<Form.Description>This is the code from your authenticator app.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={addTwoFactorForm} name="current_password">
<Form.Control let:attrs>
<Form.Label for="password">Enter Password</Form.Label>
<Input type="password" {...attrs} bind:value={$addTwoFactorFormData.current_password} />
</Form.Control>
<Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
<span>Secret: {secret}</span>
{/if}
</section>
<style lang="postcss">
section {
max-width: 20rem;
line-break: anywhere;
}
</style>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import LeftNav from '$components/LeftNav.svelte'
import type { Route } from '$lib/types'
const routes: Route[] = [
{ href: '/settings/profile', label: 'Profile' },
{ href: '/settings/security', label: 'Security' },
]
let { children } = $props()
</script>
<LeftNav {routes}>
{@render children()}
</LeftNav>

View file

@ -0,0 +1,8 @@
// +page.server.ts
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async () => {
// Redirect to a different page
throw redirect(307, '/settings/profile')
}

View file

@ -1,9 +1,6 @@
import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages'
import { usersTable } from '$lib/server/api/databases/tables' import { usersTable } from '$lib/server/api/databases/tables'
import { db } from '$lib/server/api/packages/drizzle' import { db } from '$lib/server/api/packages/drizzle'
import { changeEmailSchema, profileSchema } from '$lib/validations/account'
import { type Actions, fail } from '@sveltejs/kit' import { type Actions, fail } from '@sveltejs/kit'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server'
@ -11,6 +8,7 @@ import { zod } from 'sveltekit-superforms/adapters'
import { message, setError, superValidate } from 'sveltekit-superforms/server' import { message, setError, superValidate } from 'sveltekit-superforms/server'
import { z } from 'zod' import { z } from 'zod'
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types'
import { updateEmailSchema, updateProfileSchema } from './schemas'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event
@ -28,14 +26,14 @@ export const load: PageServerLoad = async (event) => {
// where: eq(usersTable.id, user!.id!), // where: eq(usersTable.id, user!.id!),
// }); // });
const profileForm = await superValidate(zod(profileSchema), { const profileForm = await superValidate(zod(updateProfileSchema), {
defaults: { defaults: {
firstName: authedUser?.firstName ?? '', firstName: authedUser?.firstName ?? '',
lastName: authedUser?.lastName ?? '', lastName: authedUser?.lastName ?? '',
username: authedUser?.username ?? '', username: authedUser?.username ?? '',
}, },
}) })
const emailForm = await superValidate(zod(changeEmailSchema), { const emailForm = await superValidate(zod(updateEmailSchema), {
defaults: { defaults: {
email: authedUser?.email ?? '', email: authedUser?.email ?? '',
}, },
@ -66,7 +64,7 @@ export const actions: Actions = {
redirect(302, '/login', notSignedInMessage, event) redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(updateProfileDto)) const form = await superValidate(event, zod(updateProfileSchema))
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse) const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse)
console.log('data from profile update', error) console.log('data from profile update', error)
@ -84,7 +82,7 @@ export const actions: Actions = {
return message(form, { type: 'success', message: 'Profile updated successfully!' }) return message(form, { type: 'success', message: 'Profile updated successfully!' })
}, },
changeEmail: async (event) => { changeEmail: async (event) => {
const form = await superValidate(event, zod(updateEmailDto)) const form = await superValidate(event, zod(updateEmailSchema))
const newEmail = form.data?.email const newEmail = form.data?.email
if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) { if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) {

View file

@ -1,27 +1,24 @@
<script lang="ts"> <script lang="ts">
import * as Alert from '$components/ui/alert'
import { Button } from '$components/ui/button' import { Button } from '$components/ui/button'
import { Input } from '$components/ui/input' import { Input } from '$components/ui/input'
import * as Alert from '$lib/components/ui/alert'
// import * as Form from '$lib/components/ui/form'; // import * as Form from '$lib/components/ui/form';
import { Label } from '$lib/components/ui/label' import { Label } from '$components/ui/label'
import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { AlertTriangle, KeyRound } from 'lucide-svelte' import { AlertTriangle, KeyRound } from 'lucide-svelte'
import * as flashModule from 'sveltekit-flash-message/client' import * as flashModule from 'sveltekit-flash-message/client'
import { zodClient } from 'sveltekit-superforms/adapters' import { zodClient } from 'sveltekit-superforms/adapters'
import { superForm } from 'sveltekit-superforms/client' import { superForm } from 'sveltekit-superforms/client'
import { updateEmailSchema, updateProfileSchema } from './schemas'
const { data } = $props() const { data } = $props()
const hasSetupTwoFactor = data.hasSetupTwoFactor
const { const {
form: profileForm, form: profileForm,
errors: profileErrors, errors: profileErrors,
enhance: profileEnhance, enhance: profileEnhance,
} = superForm(data.profileForm, { } = superForm(data.profileForm, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(updateProfileDto), validators: zodClient(updateProfileSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
syncFlashMessage: true, syncFlashMessage: true,
@ -36,7 +33,7 @@ const {
enhance: emailEnhance, enhance: emailEnhance,
} = superForm(data.emailForm, { } = superForm(data.emailForm, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(updateEmailDto), validators: zodClient(updateEmailSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
syncFlashMessage: true, syncFlashMessage: true,
@ -101,27 +98,6 @@ const {
{/if} {/if}
</div> </div>
</form> </form>
<div class="mt-6">
{#if !hasSetupTwoFactor}
<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 Multi-factor Authentication
</Button>
{:else}
<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 Multi-factor Authentication
</Button>
{/if}
</div>
<div class="mt-6">
<Button variant="link" class="text-secondary-foreground" href="/profile/security/password/change">
<KeyRound class="mr-2 h-4 w-4" />
Change Password
</Button>
</div>
<style lang="postcss"> <style lang="postcss">
form { form {

View file

@ -0,0 +1,20 @@
import { z } from 'zod'
export const updateEmailSchema = z.object({
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).email({ message: 'Please enter a valid email' }),
})
export type UpdateEmailSchema = z.infer<typeof updateEmailSchema>
export const updateProfileSchema = z.object({
firstName: z
.string()
.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' })
.optional(),
lastName: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
})
export type UpdateProfileSchema = z.infer<typeof updateProfileSchema>

View file

@ -0,0 +1,18 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { redirect } from 'sveltekit-flash-message/server'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async (event) => {
const { locals } = event
const authedUser = await locals.getAuthedUser()
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
}
return {
hasSetupTwoFactor: authedUser.mfa_enabled,
}
}
export const actions = {}

View file

@ -0,0 +1,30 @@
<script>
import { Button } from '$components/ui/button/index'
import { KeyRound } from 'lucide-svelte'
const { data } = $props()
const hasSetupTwoFactor = data.hasSetupTwoFactor
</script>
<div class="mt-6">
{#if !hasSetupTwoFactor}
<p>Multi Factor Authentication is: <strong>Disabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/settings/security/mfa">
<KeyRound class="mr-2 h-4 w-4" />
Setup Multi-factor Authentication
</Button>
{:else}
<p>Multi Factor Authentication is: <strong>Enabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/settings/security/mfa">
<KeyRound class="mr-2 h-4 w-4" />
Disable Multi-factor Authentication
</Button>
{/if}
</div>
<div class="mt-6">
<Button variant="link" class="text-secondary-foreground" href="/settings/security/change/password">
<KeyRound class="mr-2 h-4 w-4" />
Change Password
</Button>
</div>

View file

@ -1,6 +1,6 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages'
import { usersTable } from '$lib/server/api/databases/tables'
import { db } from '$lib/server/api/packages/drizzle' import { db } from '$lib/server/api/packages/drizzle'
import { changeUserPasswordSchema } from '$lib/validations/account'
import { type Actions, fail } from '@sveltejs/kit' import { type Actions, fail } from '@sveltejs/kit'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import type { Cookie } from 'lucia' import type { Cookie } from 'lucia'
@ -8,8 +8,8 @@ import { Argon2id } from 'oslo/password'
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server'
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters'
import { setError, superValidate } from 'sveltekit-superforms/server' import { setError, superValidate } from 'sveltekit-superforms/server'
import type { PageServerLoad } from '../../../$types' import type { PageServerLoad } from './$types'
import { usersTable } from '../../../../../../../lib/server/api/databases/tables' import { changeUserPasswordSchema } from './schemas'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event

View file

@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters'; import * as Alert from '$components/ui/alert'
import { superForm } from 'sveltekit-superforms/client'; import * as Form from '$components/ui/form'
import { AlertTriangle } from 'lucide-svelte'; import { Input } from '$components/ui/input'
import * as Alert from "$components/ui/alert"; import { AlertTriangle } from 'lucide-svelte'
import * as Form from '$components/ui/form'; import { zodClient } from 'sveltekit-superforms/adapters'
import { Input } from '$components/ui/input'; import { superForm } from 'sveltekit-superforms/client'
import { changeUserPasswordSchema } from '$lib/validations/account'; import { changeUserPasswordSchema } from './schemas'
export let data; const { data } = $props()
const form = superForm(data.form, { const form = superForm(data.form, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(changeUserPasswordSchema), validators: zodClient(changeUserPasswordSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent' multipleSubmits: 'prevent',
}); })
const { form: formData, enhance } = form; const { form: formData, enhance } = form
</script> </script>
<form method="POST" use:enhance> <form method="POST" use:enhance>

View file

@ -0,0 +1,81 @@
import { z } from 'zod'
export const changeUserPasswordSchema = z
.object({
current_password: z.string({ required_error: 'Current Password is required' }),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx)
})
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema
const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
comparePasswords(confirm_password, password, ctx)
checkPasswordStrength(password, ctx)
}
const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
if (confirm_password !== password) {
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['confirm_password'],
})
}
}
const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) => {
const minimumLength = password.length < 8
const maximumLength = password.length > 128
const containsUppercase = (ch: string) => /[A-Z]/.test(ch)
const containsLowercase = (ch: string) => /[a-z]/.test(ch)
const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch)
let countOfUpperCase = 0
let countOfLowerCase = 0
let countOfNumbers = 0
let countOfSpecialChar = 0
for (let i = 0; i < password.length; i++) {
const char = password.charAt(i)
if (!Number.isNaN(+char)) {
countOfNumbers++
} else if (containsUppercase(char)) {
countOfUpperCase++
} else if (containsLowercase(char)) {
countOfLowerCase++
} else if (containsSpecialChar(char)) {
countOfSpecialChar++
}
}
let errorMessage = 'Your password:'
if (countOfLowerCase < 1) {
errorMessage = ' Must have at least one lowercase letter. '
}
if (countOfNumbers < 1) {
errorMessage += ' Must have at least one number. '
}
if (countOfUpperCase < 1) {
errorMessage += ' Must have at least one uppercase letter. '
}
if (countOfSpecialChar < 1) {
errorMessage += ' Must have at least one special character.'
}
if (minimumLength) {
errorMessage += ' Be at least 8 characters long.'
}
if (maximumLength) {
errorMessage += ' Be less than 128 characters long.'
}
if (errorMessage.length > 'Your password:'.length) {
ctx.addIssue({
code: 'custom',
message: errorMessage,
path: ['password'],
})
}
}

View file

@ -0,0 +1,25 @@
import { notSignedInMessage } from '$lib/flashMessages'
import env from '$lib/server/api/common/env'
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account'
import { type Actions, fail } from '@sveltejs/kit'
import kebabCase from 'just-kebab-case'
import { base32, decodeHex } from 'oslo/encoding'
import { createTOTPKeyURI } from 'oslo/otp'
import QRCode from 'qrcode'
import { redirect } from 'sveltekit-flash-message/server'
import { zod } from 'sveltekit-superforms/adapters'
import { setError, superValidate } from 'sveltekit-superforms/server'
import type { PageServerLoad } from '../../$types'
export const load: PageServerLoad = async (event) => {
const { locals } = event
const authedUser = await locals.getAuthedUser()
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
}
return {}
}
export const actions: Actions = {}

View file

@ -0,0 +1,57 @@
<script lang="ts">
import { Badge } from '$components/ui/badge'
import { Button } from '$components/ui/button'
import * as Card from '$lib/components/ui/card'
const { data } = $props()
const totpEnabled = true
const hardwareTokenEnabled = true
</script>
<h1>Two-factor authentication</h1>
<Card.Root>
<Card.Header>
<Card.Title>Two-Factor Methods</Card.Title>
</Card.Header>
<Card.Content>
<section>
<div class="two-factor-method">
<div class="two-factor-method-content">
<h2>Authenticator app {#if hardwareTokenEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
<p>Use an authenticator app or browser extension to get two-factor authentication codes when prompted.</p>
</div>
<Button href="/settings/security/mfa/totp">Edit</Button>
</div>
<div class="two-factor-method">
<div class="two-factor-method-content">
<h2>Security Keys {#if hardwareTokenEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
<p>
Security keys are webauthn credentials that can only be used as a second factor of authentication.
</p>
</div>
<Button href="/settings/security/mfa/security-keys">Edit</Button>
</div>
</section>
</Card.Content>
</Card.Root>
<style lang="postcss">
section {
display: grid;
gap: 1rem;
}
.two-factor-method {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
}
.two-factor-method-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View file

@ -1,6 +1,5 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages'
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account' import env from '$lib/server/api/common/env'
import env from '$src/env'
import { type Actions, fail } from '@sveltejs/kit' import { type Actions, fail } from '@sveltejs/kit'
import kebabCase from 'just-kebab-case' import kebabCase from 'just-kebab-case'
import { base32, decodeHex } from 'oslo/encoding' import { base32, decodeHex } from 'oslo/encoding'
@ -10,6 +9,7 @@ import { redirect } from 'sveltekit-flash-message/server'
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters'
import { setError, superValidate } from 'sveltekit-superforms/server' import { setError, superValidate } from 'sveltekit-superforms/server'
import type { PageServerLoad } from '../../$types' import type { PageServerLoad } from '../../$types'
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event
@ -125,7 +125,7 @@ export const actions: Actions = {
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code') return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code')
} }
redirect(302, '/profile/security/mfa/recovery-codes') redirect(302, '/settings/security/mfa/recovery-codes')
}, },
disableTotp: async (event) => { disableTotp: async (event) => {
const { locals } = event const { locals } = event
@ -162,7 +162,7 @@ export const actions: Actions = {
redirect( redirect(
302, 302,
'/profile/security/mfa', '/settings/security/mfa',
{ {
type: 'success', type: 'success',
message: 'Two-Factor Authentication has been disabled.', message: 'Two-Factor Authentication has been disabled.',

View file

@ -0,0 +1,83 @@
<script lang="ts">
import PinInput from '$components/pin-input.svelte'
import * as Alert from '$components/ui/alert'
import * as Form from '$components/ui/form'
import { Input } from '$components/ui/input'
import { AlertTriangle } from 'lucide-svelte'
import { zodClient } from 'sveltekit-superforms/adapters'
import { superForm } from 'sveltekit-superforms/client'
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas'
const { data } = $props()
const { qrCode, secret, twoFactorEnabled, recoveryCodes } = data
const addTwoFactorForm = superForm(data.addTwoFactorForm, {
taintedMessage: null,
validators: zodClient(addTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
})
const removeTwoFactorForm = superForm(data.removeTwoFactorForm, {
taintedMessage: null,
validators: zodClient(removeTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
})
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes)
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm
</script>
<section class="two-factor">
<h1>Two-Factor Authentication</h1>
{#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="?/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>
<Input type="password" {...attrs} bind:value={$removeTwoFactorFormData.current_password} />
</Form.Control>
<Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Disable Two Factor Authentication</Form.Button>
</form>
{:else}
<h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" />
<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>
<PinInput {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
</Form.Control>
<Form.Description>This is the code from your authenticator app.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={addTwoFactorForm} name="current_password">
<Form.Control let:attrs>
<Form.Label for="password">Enter Password</Form.Label>
<Input type="password" {...attrs} bind:value={$addTwoFactorFormData.current_password} />
</Form.Control>
<Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
<span>Secret: {secret}</span>
{/if}
</section>
<style lang="postcss">
section {
max-width: 20rem;
line-break: anywhere;
}
</style>

View file

@ -0,0 +1,14 @@
import { z } from 'zod'
export const addTwoFactorSchema = z.object({
current_password: z.string({ required_error: 'Current Password is required' }),
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
})
export type AddTwoFactorSchema = typeof addTwoFactorSchema
export const removeTwoFactorSchema = addTwoFactorSchema.pick({
current_password: true,
})
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema

View file

@ -90,18 +90,20 @@ export const actions: Actions = {
// Create new wishlist // Create new wishlist
create: async (event) => { create: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
// Delete a wishlist // Delete a wishlist
delete: async (event) => { delete: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
@ -116,7 +118,7 @@ export const actions: Actions = {
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
try { try {
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -131,7 +133,7 @@ export const actions: Actions = {
} }
if (game) { if (game) {
const wishlist = await db.query.wishlists.findFirst({ const wishlist = await db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.user_id, authedUser.id), where: eq(wishlistsTable.user_id, authedUser.id),
}) })

View file

@ -51,14 +51,15 @@ export const actions: Actions = {
// Add game to a wishlist // Add game to a wishlist
add: async (event) => { add: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
try { try {
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -73,8 +74,8 @@ export const actions: Actions = {
} }
if (game) { if (game) {
const wishlist = await db.query.wishlists.findFirst({ const wishlist = await db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.user_id, user!.id!), where: eq(wishlistsTable.user_id, authedUser.id),
}) })
if (!wishlist) { if (!wishlist) {
@ -99,32 +100,35 @@ export const actions: Actions = {
// Create new wishlist // Create new wishlist
create: async (event) => { create: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
// Delete a wishlist // Delete a wishlist
delete: async (event) => { delete: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
return error(405, 'Method not allowed') return error(405, 'Method not allowed')
}, },
// Remove game from a wishlist // Remove game from a wishlist
remove: async (event) => { remove: async (event) => {
const { locals } = event const { locals } = event
const { user, session } = locals
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
return fail(401) if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
} }
const form = await superValidate(event, zod(modifyListGameSchema)) const form = await superValidate(event, zod(modifyListGameSchema))
try { try {
const game = await db.query.games.findFirst({ const game = await db.query.gamesTable.findFirst({
where: eq(gamesTable.id, form.data.id), where: eq(gamesTable.id, form.data.id),
}) })
@ -139,8 +143,8 @@ export const actions: Actions = {
} }
if (game) { if (game) {
const wishlist = await db.query.wishlists.findFirst({ const wishlist = await db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.user_id, user!.id!), where: eq(wishlistsTable.user_id, authedUser.id),
}) })
if (!wishlist) { if (!wishlist) {

View file

@ -47,7 +47,9 @@ export const actions: Actions = {
const form = await superValidate(event, zod(signinUsernameDto)) const form = await superValidate(event, zod(signinUsernameDto))
const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse) const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse)
if (error) return setError(form, 'username', error) if (error) {
return setError(form, 'username', error)
}
if (!form.valid) { if (!form.valid) {
form.data.password = '' form.data.password = ''

View file

@ -1,57 +1,56 @@
import { fail, error, type Actions } from '@sveltejs/kit'; import { StatusCodes } from '$lib/constants/status-codes'
import { zod } from 'sveltekit-superforms/adapters'; import { notSignedInMessage } from '$lib/flashMessages'
import { setError, superValidate } from 'sveltekit-superforms/server'; import { resetPasswordEmailSchema, resetPasswordTokenSchema } from '$lib/validations/auth'
import { redirect } from 'sveltekit-flash-message/server'; import { type Actions, error, fail } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'; import { redirect } from 'sveltekit-flash-message/server'
import {resetPasswordEmailSchema, resetPasswordTokenSchema} from "$lib/validations/auth"; import { zod } from 'sveltekit-superforms/adapters'
import {StatusCodes} from "$lib/constants/status-codes"; import { setError, superValidate } from 'sveltekit-superforms/server'
import {userFullyAuthenticated} from "$lib/server/auth-utils"; import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
return { return {
emailForm: await superValidate(zod(resetPasswordEmailSchema)), emailForm: await superValidate(zod(resetPasswordEmailSchema)),
tokenForm: await superValidate(zod(resetPasswordTokenSchema)), tokenForm: await superValidate(zod(resetPasswordTokenSchema)),
}; }
}; }
export const actions = { export const actions = {
passwordReset: async (event) => { passwordReset: async (event) => {
const { request, locals } = event; const { request, locals } = event
const { user, session } = locals;
if (userFullyAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
const message = { type: 'success', message: 'You are already signed in' } as const; if (!authedUser) {
throw redirect('/', message, event); throw redirect(302, '/login', notSignedInMessage, event)
} }
const emailForm = await superValidate(request, zod(resetPasswordEmailSchema)); const emailForm = await superValidate(request, zod(resetPasswordEmailSchema))
if (!emailForm.valid) { if (!emailForm.valid) {
return fail(StatusCodes.BAD_REQUEST, { emailForm }); return fail(StatusCodes.BAD_REQUEST, { emailForm })
} }
// const error = {}; // const error = {};
// // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse); // // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
// if (error) { // if (error) {
// return setError(emailForm, 'email', error); // return setError(emailForm, 'email', error);
// } // }
return { emailForm }; return { emailForm }
}, },
verifyToken: async (event) => { verifyToken: async (event) => {
const { request, locals } = event; const { request, locals } = event
const { user, session } = locals;
if (userFullyAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser()
const message = { type: 'success', message: 'You are already signed in' } as const; if (!authedUser) {
throw redirect('/', message, event); throw redirect(302, '/login', notSignedInMessage, event)
} }
const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema)); const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema))
if (!tokenForm.valid) { if (!tokenForm.valid) {
return fail(StatusCodes.BAD_REQUEST, { tokenForm }); return fail(StatusCodes.BAD_REQUEST, { tokenForm })
} }
const error = {}; const error = {}
// const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse) // const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse)
if (error) { if (error) {
return setError(tokenForm, 'token', error); return setError(tokenForm, 'token', error)
} }
redirect(301, '/'); redirect(301, '/')
} },
}; }

View file

@ -1,4 +1,5 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages'
import env from '$lib/server/api/common/env'
import { twoFactorTable, usersTable } from '$lib/server/api/databases/tables' import { twoFactorTable, usersTable } from '$lib/server/api/databases/tables'
import { db } from '$lib/server/api/packages/drizzle' import { db } from '$lib/server/api/packages/drizzle'
import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth' import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth'
@ -9,7 +10,6 @@ import { redirect } from 'sveltekit-flash-message/server'
import { RateLimiter } from 'sveltekit-rate-limiter/server' import { RateLimiter } from 'sveltekit-rate-limiter/server'
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters'
import { superValidate } from 'sveltekit-superforms/server' import { superValidate } from 'sveltekit-superforms/server'
import env from '../../../env'
import type { PageServerLoad, RequestEvent } from './$types' import type { PageServerLoad, RequestEvent } from './$types'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {

View file

@ -25,6 +25,7 @@ const config = {
$db: './src/lib/server/api/infrastructure/database', $db: './src/lib/server/api/infrastructure/database',
$server: './src/server', $server: './src/server',
$lib: './src/lib', $lib: './src/lib',
$routes: './src/routes',
$src: './src', $src: './src',
$state: './src/state', $state: './src/state',
$styles: './src/styles', $styles: './src/styles',