mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Moving around settings and security, fixing left nav to static list, fixing recovery codes generation.
This commit is contained in:
parent
af59ee1cfd
commit
2eee00a20d
61 changed files with 902 additions and 668 deletions
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
},
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/lib/styles/app.pcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils/ui"
|
||||
},
|
||||
"typescript": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dotenv/config'
|
||||
import env from '$lib/server/api/common/env'
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
import env from './src/env'
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"@faker-js/faker": "^8.4.1",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@playwright/test": "^1.47.0",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/enhanced-img": "^0.3.4",
|
||||
"@sveltejs/kit": "^2.5.26",
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
"drizzle-kit": "^0.23.2",
|
||||
"eslint": "^8.57.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-debounce-it": "^3.2.0",
|
||||
"lucia": "3.2.0",
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
"arctic": "^1.9.2",
|
||||
"bits-ui": "^0.21.13",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"bullmq": "^5.12.13",
|
||||
"bullmq": "^5.12.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie": "^0.6.0",
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ importers:
|
|||
specifier: ^1.9.1
|
||||
version: 1.9.1
|
||||
bullmq:
|
||||
specifier: ^5.12.13
|
||||
version: 5.12.13
|
||||
specifier: ^5.12.14
|
||||
version: 5.12.14
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
|
|
@ -181,8 +181,8 @@ importers:
|
|||
specifier: ^0.83.0
|
||||
version: 0.83.0(svelte@5.0.0-next.175)
|
||||
'@playwright/test':
|
||||
specifier: ^1.46.1
|
||||
version: 1.46.1
|
||||
specifier: ^1.47.0
|
||||
version: 1.47.0
|
||||
'@sveltejs/adapter-auto':
|
||||
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)))
|
||||
|
|
@ -223,8 +223,8 @@ importers:
|
|||
specifier: ^9.1.0
|
||||
version: 9.1.0(eslint@8.57.0)
|
||||
eslint-plugin-svelte:
|
||||
specifier: ^2.43.0
|
||||
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))
|
||||
specifier: 2.36.0-next.13
|
||||
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:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
|
|
@ -1706,8 +1706,8 @@ packages:
|
|||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.46.1':
|
||||
resolution: {integrity: sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==}
|
||||
'@playwright/test@1.47.0':
|
||||
resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -2295,8 +2295,8 @@ packages:
|
|||
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
bullmq@5.12.13:
|
||||
resolution: {integrity: sha512-bFk0s1U9eQ8vKrhH9zYg/1H0+puSLVXuuq/pIW2jxgUmtLebRUBZr0cHJx35azTf2oPUJ+xXfpfHWaUtm4ZveA==}
|
||||
bullmq@5.12.14:
|
||||
resolution: {integrity: sha512-mcSQHq9EY+DKtAP6XSmkP+0f1ifFithcpLTwo8WmUauArE9dxk45Gae3Fls1Nwf0Er9MoaDhPcglfe6LV/XCOg==}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
|
|
@ -2769,12 +2769,12 @@ packages:
|
|||
peerDependencies:
|
||||
eslint: '>=7.0.0'
|
||||
|
||||
eslint-plugin-svelte@2.43.0:
|
||||
resolution: {integrity: sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==}
|
||||
eslint-plugin-svelte@2.36.0-next.13:
|
||||
resolution: {integrity: sha512-N4bLGdFkGbbAQiKvX17kLfBgnZ+Em00khOY3AReppO7fkP9jaSxwjdgTCcWf+Q5/uZWor58g4GleRqHcb2Dk2w==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
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:
|
||||
svelte:
|
||||
optional: true
|
||||
|
|
@ -3212,8 +3212,8 @@ packages:
|
|||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
known-css-properties@0.34.0:
|
||||
resolution: {integrity: sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==}
|
||||
known-css-properties@0.30.0:
|
||||
resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
|
|
@ -3673,13 +3673,13 @@ packages:
|
|||
pkg-types@1.2.0:
|
||||
resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==}
|
||||
|
||||
playwright-core@1.46.1:
|
||||
resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==}
|
||||
playwright-core@1.47.0:
|
||||
resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.46.1:
|
||||
resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==}
|
||||
playwright@1.47.0:
|
||||
resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -5888,9 +5888,9 @@ snapshots:
|
|||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.46.1':
|
||||
'@playwright/test@1.47.0':
|
||||
dependencies:
|
||||
playwright: 1.46.1
|
||||
playwright: 1.47.0
|
||||
|
||||
'@polka/url@1.0.0-next.25': {}
|
||||
|
||||
|
|
@ -6508,7 +6508,7 @@ snapshots:
|
|||
|
||||
builtin-modules@3.3.0: {}
|
||||
|
||||
bullmq@5.12.13:
|
||||
bullmq@5.12.14:
|
||||
dependencies:
|
||||
cron-parser: 4.9.0
|
||||
ioredis: 5.4.1
|
||||
|
|
@ -6936,14 +6936,15 @@ snapshots:
|
|||
dependencies:
|
||||
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:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
debug: 4.3.6
|
||||
eslint: 8.57.0
|
||||
eslint-compat-utils: 0.5.1(eslint@8.57.0)
|
||||
esutils: 2.0.3
|
||||
known-css-properties: 0.34.0
|
||||
known-css-properties: 0.30.0
|
||||
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-safe-parser: 6.0.0(postcss@8.4.45)
|
||||
|
|
@ -6953,6 +6954,7 @@ snapshots:
|
|||
optionalDependencies:
|
||||
svelte: 5.0.0-next.175
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
eslint-scope@7.2.2:
|
||||
|
|
@ -7479,7 +7481,7 @@ snapshots:
|
|||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
known-css-properties@0.34.0: {}
|
||||
known-css-properties@0.30.0: {}
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
|
|
@ -7882,11 +7884,11 @@ snapshots:
|
|||
mlly: 1.7.1
|
||||
pathe: 1.1.2
|
||||
|
||||
playwright-core@1.46.1: {}
|
||||
playwright-core@1.47.0: {}
|
||||
|
||||
playwright@1.46.1:
|
||||
playwright@1.47.0:
|
||||
dependencies:
|
||||
playwright-core: 1.46.1
|
||||
playwright-core: 1.47.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$components/ui/dropdown-menu';
|
||||
import * as Avatar from '$components/ui/avatar';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import Logo from '$components/logo.svelte';
|
||||
import type { Users } from '$db/schema';
|
||||
import { applyAction, enhance } from '$app/forms'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
import Logo from '$components/logo.svelte'
|
||||
import * as Avatar from '$components/ui/avatar'
|
||||
import * as DropdownMenu from '$components/ui/dropdown-menu'
|
||||
import type { Users } from '$db/schema'
|
||||
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'
|
||||
import toast from 'svelte-french-toast'
|
||||
|
||||
type HeaderProps = {
|
||||
user: Users | null;
|
||||
};
|
||||
type HeaderProps = {
|
||||
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>
|
||||
|
||||
<header>
|
||||
|
|
@ -40,10 +40,10 @@
|
|||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Account</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<a href="/profile">
|
||||
<a href="/settings">
|
||||
<DropdownMenu.Item>
|
||||
<User class="mr-2 h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
<span>Settings</span>
|
||||
</DropdownMenu.Item>
|
||||
</a>
|
||||
<a href="/collections">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
|
||||
type Route = {
|
||||
href: string
|
||||
label: string
|
||||
}
|
||||
import type { Route } from '$lib/types'
|
||||
|
||||
let { children, routes }: { children: unknown; routes: Route[] } = $props()
|
||||
</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">
|
||||
<nav>
|
||||
<ul>
|
||||
{#each routes as { href, label }}
|
||||
<li>
|
||||
<a href={href} class:active={$page.url.pathname === href}>
|
||||
<a href={href} class:active={$page.url.pathname.includes(href)}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -62,8 +62,8 @@ let { children, routes }: { children: unknown; routes: Route[] } = $props()
|
|||
}
|
||||
|
||||
&.active {
|
||||
color: #23527c;
|
||||
font-weight: bold;
|
||||
color: var(--color-link-hover);
|
||||
font-weight: 600;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { badgeVariants, type Variant } from ".";
|
||||
import { type Variant, badgeVariants } from "./index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export let href: string | undefined = undefined;
|
||||
|
|
|
|||
|
|
@ -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 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: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
outline: "text-foreground"
|
||||
}
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof badgeVariants>["variant"];
|
||||
|
|
|
|||
38
src/lib/server/api/common/config.ts
Normal file
38
src/lib/server/api/common/config.ts
Normal 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)
|
||||
|
|
@ -10,8 +10,6 @@ const stringBoolean = z.coerce
|
|||
.default('false')
|
||||
|
||||
const EnvSchema = z.object({
|
||||
ADMIN_USERNAME: z.string(),
|
||||
ADMIN_PASSWORD: z.string(),
|
||||
DATABASE_USER: z.string(),
|
||||
DATABASE_PASSWORD: z.string(),
|
||||
DATABASE_HOST: z.string(),
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
export interface Config {
|
||||
isProduction: boolean
|
||||
domain: string
|
||||
api: ApiConfig
|
||||
storage: StorageConfig
|
||||
// storage: StorageConfig
|
||||
redis: RedisConfig
|
||||
postgres: PostgresConfig
|
||||
}
|
||||
|
|
@ -10,17 +11,23 @@ interface ApiConfig {
|
|||
origin: string
|
||||
}
|
||||
|
||||
interface StorageConfig {
|
||||
accessKey: string
|
||||
secretKey: string
|
||||
bucket: string
|
||||
url: string
|
||||
}
|
||||
// interface StorageConfig {
|
||||
// accessKey: string
|
||||
// secretKey: string
|
||||
// bucket: string
|
||||
// url: string
|
||||
// }
|
||||
|
||||
interface RedisConfig {
|
||||
url: string
|
||||
}
|
||||
|
||||
interface PostgresConfig {
|
||||
url: string
|
||||
user: string
|
||||
password: string
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
ssl: boolean
|
||||
max: number | undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -49,8 +49,9 @@ export class MfaController extends Controller {
|
|||
const user = c.var.user
|
||||
// You can only view recovery codes once and that is on creation
|
||||
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id)
|
||||
if (existingCodes) {
|
||||
return c.body('You have already generated recovery codes', StatusCodes.BAD_REQUEST)
|
||||
if (existingCodes && existingCodes.length > 0) {
|
||||
console.log('Recovery Codes found', existingCodes)
|
||||
return c.json({ recoveryCodes: existingCodes })
|
||||
}
|
||||
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
|
||||
return c.json({ recoveryCodes })
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { drizzle } from 'drizzle-orm/postgres-js'
|
|||
import { migrate } from 'drizzle-orm/postgres-js/migrator'
|
||||
import postgres from 'postgres'
|
||||
import config from '../../../../../drizzle.config'
|
||||
import env from '../../../../env'
|
||||
import env from '../common/env'
|
||||
|
||||
const connection = postgres({
|
||||
host: env.DATABASE_HOST || 'localhost',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'reflect-metadata'
|
|||
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
|
||||
import { type Table, getTableName, sql } from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import env from '../../../../env'
|
||||
import env from '../common/env'
|
||||
import * as seeds from './seeds'
|
||||
import * as schema from './tables'
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as schema from '$lib/server/api/databases/tables'
|
|||
import type { db } from '$lib/server/api/packages/drizzle'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { Argon2id } from 'oslo/password'
|
||||
import { config } from '../../configs/config'
|
||||
import { config } from '../../common/config'
|
||||
import users from './data/users.json'
|
||||
|
||||
type JsonRole = {
|
||||
|
|
@ -18,7 +18,7 @@ export default async function seed(db: db) {
|
|||
const adminUser = await db
|
||||
.insert(schema.usersTable)
|
||||
.values({
|
||||
username: `${config.ADMIN_USERNAME}`,
|
||||
username: `${process.env.ADMIN_USERNAME}`,
|
||||
email: '',
|
||||
first_name: 'Brad',
|
||||
last_name: 'S',
|
||||
|
|
@ -32,7 +32,7 @@ export default async function seed(db: db) {
|
|||
await db.insert(schema.credentialsTable).values({
|
||||
user_id: adminUser[0].id,
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 { timestamps } from '../../common/utils/table'
|
||||
import { usersTable } from './users.table'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { hc } from 'hono/client'
|
|||
import { cors } from 'hono/cors'
|
||||
import { logger } from 'hono/logger'
|
||||
import { container } from 'tsyringe'
|
||||
import { config } from './configs/config'
|
||||
import { config } from './common/config'
|
||||
import { IamController } from './controllers/iam.controller'
|
||||
import { LoginController } from './controllers/login.controller'
|
||||
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'
|
||||
|
|
@ -45,7 +45,7 @@ const routes = app
|
|||
.route('/user', container.resolve(UserController).routes())
|
||||
.route('/login', container.resolve(LoginController).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('/mfa', container.resolve(MfaController).routes())
|
||||
.get('/', (c) => c.json({ message: 'Server is healthy' }))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import pg from 'pg'
|
||||
import { config } from '../configs/config'
|
||||
import { config } from '../common/config'
|
||||
import * as schema from '../databases/tables'
|
||||
|
||||
// create the connection
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import RedisClient from 'ioredis'
|
||||
import { container } from 'tsyringe'
|
||||
import { config } from '../configs/config'
|
||||
import { config } from '../common/config'
|
||||
|
||||
export const RedisProvider = Symbol('REDIS_TOKEN')
|
||||
export type RedisProvider = RedisClient
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'reflect-metadata'
|
||||
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository'
|
||||
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
|
||||
import { type InferInsertModel, eq } from 'drizzle-orm'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
|
|
@ -10,6 +11,10 @@ export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable>
|
|||
export class RecoveryCodesRepository {
|
||||
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) {
|
||||
return db.query.recoveryCodesTable.findFirst({
|
||||
where: eq(recoveryCodesTable.userId, userId),
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ export class WishlistsRepository {
|
|||
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
||||
|
||||
async findAll(db = this.drizzle.db) {
|
||||
return db.query.wishlists.findMany()
|
||||
return db.query.wishlistsTable.findMany()
|
||||
}
|
||||
|
||||
async findOneById(id: string, db = this.drizzle.db) {
|
||||
return db.query.wishlists.findFirst({
|
||||
return db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.id, id),
|
||||
columns: {
|
||||
cuid: true,
|
||||
|
|
@ -26,7 +26,7 @@ export class WishlistsRepository {
|
|||
}
|
||||
|
||||
async findOneByCuid(cuid: string, db = this.drizzle.db) {
|
||||
return db.query.wishlists.findFirst({
|
||||
return db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.cuid, cuid),
|
||||
columns: {
|
||||
cuid: true,
|
||||
|
|
@ -36,7 +36,7 @@ export class WishlistsRepository {
|
|||
}
|
||||
|
||||
async findOneByUserId(userId: string, db = this.drizzle.db) {
|
||||
return db.query.wishlists.findFirst({
|
||||
return db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.user_id, userId),
|
||||
columns: {
|
||||
cuid: true,
|
||||
|
|
@ -46,7 +46,7 @@ export class WishlistsRepository {
|
|||
}
|
||||
|
||||
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
||||
return db.query.wishlists.findMany({
|
||||
return db.query.wishlistsTable.findMany({
|
||||
where: eq(wishlistsTable.user_id, userId),
|
||||
columns: {
|
||||
cuid: true,
|
||||
|
|
|
|||
|
|
@ -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 { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres'
|
||||
import pg from 'pg'
|
||||
|
|
@ -12,18 +12,18 @@ export class DrizzleService implements Disposable {
|
|||
|
||||
constructor() {
|
||||
const pool = new pg.Pool({
|
||||
user: config.DATABASE_USER,
|
||||
password: config.DATABASE_PASSWORD,
|
||||
host: config.DATABASE_HOST,
|
||||
port: Number(config.DATABASE_PORT).valueOf(),
|
||||
database: config.DATABASE_DB,
|
||||
ssl: config.DATABASE_HOST !== 'localhost',
|
||||
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined,
|
||||
user: config.postgres.user,
|
||||
password: config.postgres.password,
|
||||
host: config.postgres.host,
|
||||
port: Number(config.postgres.port).valueOf(),
|
||||
database: config.postgres.database,
|
||||
ssl: config.postgres.ssl,
|
||||
max: config.postgres.max,
|
||||
})
|
||||
this.pool = pool
|
||||
this.db = drizzle(pool, {
|
||||
schema,
|
||||
logger: config.NODE_ENV === 'development',
|
||||
logger: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'
|
||||
import { Lucia, TimeSpan } from 'lucia'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { injectable } from 'tsyringe'
|
||||
import { config } from '../common/config'
|
||||
import type { Email } from '../common/inferfaces/email.interface'
|
||||
import { config } from '../configs/config'
|
||||
|
||||
type SendProps = {
|
||||
to: string | string[]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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 { alphabet, generateRandomString } from 'oslo/crypto'
|
||||
import { Argon2id } from 'oslo/password'
|
||||
|
|
@ -20,10 +18,7 @@ export class RecoveryCodesService {
|
|||
for (const code of createdRecoveryCodes) {
|
||||
const hashedCode = await new Argon2id().hash(code)
|
||||
console.log('Inserting recovery code', code, hashedCode)
|
||||
await db.insert(recoveryCodesTable).values({
|
||||
userId,
|
||||
code: hashedCode,
|
||||
})
|
||||
await this.recoveryCodesRepository.create({ userId, code: hashedCode })
|
||||
}
|
||||
|
||||
return createdRecoveryCodes
|
||||
|
|
|
|||
|
|
@ -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 { type Disposable, injectable } from 'tsyringe'
|
||||
|
||||
|
|
|
|||
276
src/lib/types.ts
276
src/lib/types.ts
|
|
@ -1,31 +1,36 @@
|
|||
import type { SvelteComponent } from 'svelte';
|
||||
import { collections } from '$db/schema';
|
||||
import type { collections } from '$lib/server/api/databases/tables'
|
||||
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 = {
|
||||
isOpen: boolean;
|
||||
content?: typeof SvelteComponent<any>;
|
||||
additionalData?: SavedGameType | GameType;
|
||||
};
|
||||
isOpen: boolean
|
||||
content?: typeof SvelteComponent<any>
|
||||
additionalData?: SavedGameType | GameType
|
||||
}
|
||||
|
||||
export type Search = {
|
||||
name: string;
|
||||
minAge: string;
|
||||
minPlayers: string;
|
||||
maxPlayers: string;
|
||||
exactMinAge: string;
|
||||
exactMinPlayers: string;
|
||||
exactMaxPlayers: string;
|
||||
skip: number;
|
||||
currentPage: number;
|
||||
limit: number;
|
||||
};
|
||||
name: string
|
||||
minAge: string
|
||||
minPlayers: string
|
||||
maxPlayers: string
|
||||
exactMinAge: string
|
||||
exactMinPlayers: string
|
||||
exactMaxPlayers: string
|
||||
skip: number
|
||||
currentPage: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export type BoredStore = {
|
||||
loading: boolean;
|
||||
dialog: Dialog;
|
||||
};
|
||||
loading: boolean
|
||||
dialog: Dialog
|
||||
}
|
||||
|
||||
export enum ToastType {
|
||||
INFO = 'INFO',
|
||||
|
|
@ -34,146 +39,141 @@ export enum ToastType {
|
|||
}
|
||||
|
||||
export type ToastData = {
|
||||
id: number;
|
||||
duration: number;
|
||||
dismissible: boolean;
|
||||
showButton: boolean;
|
||||
autoDismiss: boolean;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
};
|
||||
id: number
|
||||
duration: number
|
||||
dismissible: boolean
|
||||
showButton: boolean
|
||||
autoDismiss: boolean
|
||||
type: ToastType
|
||||
message: string
|
||||
}
|
||||
|
||||
export type GameMechanic = {
|
||||
id: string;
|
||||
name: string;
|
||||
boardGameAtlasLink: string;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
boardGameAtlasLink: string
|
||||
}
|
||||
|
||||
export type SavedGameType = {
|
||||
id: string;
|
||||
name: string;
|
||||
thumb_url: string;
|
||||
players: string;
|
||||
playtime: string;
|
||||
mechanics: GameMechanic[];
|
||||
searchTerms: string;
|
||||
includeInRandom: boolean;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
thumb_url: string
|
||||
players: string
|
||||
playtime: string
|
||||
mechanics: GameMechanic[]
|
||||
searchTerms: string
|
||||
includeInRandom: boolean
|
||||
}
|
||||
|
||||
export type ListGameType = {
|
||||
id: string;
|
||||
game_id: string;
|
||||
collection_id: string | undefined;
|
||||
wishlist_id: string | undefined;
|
||||
times_played: number;
|
||||
thumb_url: string;
|
||||
};
|
||||
id: string
|
||||
game_id: string
|
||||
collection_id: string | undefined
|
||||
wishlist_id: string | undefined
|
||||
times_played: number
|
||||
thumb_url: string
|
||||
}
|
||||
|
||||
export type MechanicType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type CategoryType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type PublisherType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type DesignerType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ArtistType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ExpansionType = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
export type BGGLinkType =
|
||||
| 'boardgamecategory'
|
||||
| 'boardgamemechanic'
|
||||
| 'boardgameexpansion'
|
||||
| 'boardgameartist'
|
||||
| 'boardgamepublisher';
|
||||
export type BGGLinkType = 'boardgamecategory' | 'boardgamemechanic' | 'boardgameexpansion' | 'boardgameartist' | 'boardgamepublisher'
|
||||
|
||||
export type BGGLink = {
|
||||
id: number;
|
||||
type: BGGLinkType;
|
||||
value: string;
|
||||
};
|
||||
id: number
|
||||
type: BGGLinkType
|
||||
value: string
|
||||
}
|
||||
|
||||
export type GameType = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
edit_url: string;
|
||||
thumb_url: string;
|
||||
image_url: string;
|
||||
price: number;
|
||||
price_ca: number;
|
||||
price_uk: number;
|
||||
price_au: number;
|
||||
msrp: number;
|
||||
year_published: number;
|
||||
categories: CategoryType[];
|
||||
mechanics: MechanicType[];
|
||||
primary_publisher: PublisherType;
|
||||
publishers: PublisherType[];
|
||||
primary_designer: DesignerType;
|
||||
designers: DesignerType[];
|
||||
developers: String[];
|
||||
artists: ArtistType[];
|
||||
expansions: ExpansionType[];
|
||||
min_players: number;
|
||||
max_players: number;
|
||||
min_playtime: number;
|
||||
max_playtime: number;
|
||||
min_age: number;
|
||||
description: string;
|
||||
players: string;
|
||||
playtime: number;
|
||||
external_id: number;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
url: string
|
||||
edit_url: string
|
||||
thumb_url: string
|
||||
image_url: string
|
||||
price: number
|
||||
price_ca: number
|
||||
price_uk: number
|
||||
price_au: number
|
||||
msrp: number
|
||||
year_published: number
|
||||
categories: CategoryType[]
|
||||
mechanics: MechanicType[]
|
||||
primary_publisher: PublisherType
|
||||
publishers: PublisherType[]
|
||||
primary_designer: DesignerType
|
||||
designers: DesignerType[]
|
||||
developers: string[]
|
||||
artists: ArtistType[]
|
||||
expansions: ExpansionType[]
|
||||
min_players: number
|
||||
max_players: number
|
||||
min_playtime: number
|
||||
max_playtime: number
|
||||
min_age: number
|
||||
description: string
|
||||
players: string
|
||||
playtime: number
|
||||
external_id: number
|
||||
}
|
||||
|
||||
export type SearchQuery = {
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
ids?: string[];
|
||||
list_id?: string;
|
||||
random?: boolean;
|
||||
q?: string;
|
||||
exact?: boolean;
|
||||
designer?: string;
|
||||
publisher?: string;
|
||||
artist?: string;
|
||||
mechanics?: string;
|
||||
categories?: string;
|
||||
order_by?: string;
|
||||
ascending?: boolean;
|
||||
min_players?: number;
|
||||
max_players?: number;
|
||||
min_playtime?: number;
|
||||
max_playtime?: number;
|
||||
min_age?: number;
|
||||
year_published?: number;
|
||||
gt_min_players?: number;
|
||||
gt_max_players?: number;
|
||||
gt_min_playtime?: number;
|
||||
gt_max_playtime?: number;
|
||||
gt_min_age?: number;
|
||||
gt_year_published?: number;
|
||||
lt_min_players?: number;
|
||||
lt_max_players?: number;
|
||||
lt_min_playtime?: number;
|
||||
lt_max_playtime?: number;
|
||||
lt_min_age?: number;
|
||||
lt_year_published?: number;
|
||||
fields?: string;
|
||||
};
|
||||
limit?: number
|
||||
skip?: number
|
||||
ids?: string[]
|
||||
list_id?: string
|
||||
random?: boolean
|
||||
q?: string
|
||||
exact?: boolean
|
||||
designer?: string
|
||||
publisher?: string
|
||||
artist?: string
|
||||
mechanics?: string
|
||||
categories?: string
|
||||
order_by?: string
|
||||
ascending?: boolean
|
||||
min_players?: number
|
||||
max_players?: number
|
||||
min_playtime?: number
|
||||
max_playtime?: number
|
||||
min_age?: number
|
||||
year_published?: number
|
||||
gt_min_players?: number
|
||||
gt_max_players?: number
|
||||
gt_min_playtime?: number
|
||||
gt_max_playtime?: number
|
||||
gt_min_age?: number
|
||||
gt_year_published?: number
|
||||
lt_min_players?: number
|
||||
lt_max_players?: number
|
||||
lt_min_playtime?: number
|
||||
lt_max_playtime?: number
|
||||
lt_min_age?: number
|
||||
lt_year_published?: number
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export type UICollection = Pick<typeof collections, 'cuid' | 'name'>;
|
||||
export type UICollection = Pick<typeof collections, 'cuid' | 'name'>
|
||||
|
|
|
|||
|
|
@ -1,56 +1,53 @@
|
|||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import type { TransitionConfig } from 'svelte/transition';
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import type { TransitionConfig } from 'svelte/transition'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
type FlyAndScaleParams = {
|
||||
y?: number;
|
||||
x?: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
};
|
||||
y?: number
|
||||
x?: number
|
||||
start?: number
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export const flyAndScale = (
|
||||
node: Element,
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 },
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
export const flyAndScale = (node: Element, params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }): TransitionConfig => {
|
||||
const style = getComputedStyle(node)
|
||||
const transform = style.transform === 'none' ? '' : style.transform
|
||||
|
||||
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
|
||||
const [minA, maxA] = scaleA;
|
||||
const [minB, maxB] = scaleB;
|
||||
const [minA, maxA] = scaleA
|
||||
const [minB, maxB] = scaleB
|
||||
|
||||
const percentage = (valueA - minA) / (maxA - minA);
|
||||
const valueB = percentage * (maxB - minB) + minB;
|
||||
const percentage = (valueA - minA) / (maxA - minA)
|
||||
const valueB = percentage * (maxB - minB) + minB
|
||||
|
||||
return valueB;
|
||||
};
|
||||
return valueB
|
||||
}
|
||||
|
||||
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, '');
|
||||
};
|
||||
if (style[key] === undefined) return str
|
||||
return str + `${key}:${style[key]};`
|
||||
}, '')
|
||||
}
|
||||
|
||||
return {
|
||||
duration: params.duration ?? 200,
|
||||
delay: 0,
|
||||
css: (t) => {
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0])
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0])
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1])
|
||||
|
||||
return styleToString({
|
||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||
opacity: t,
|
||||
});
|
||||
})
|
||||
},
|
||||
easing: cubicOut,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +1,69 @@
|
|||
import { z } from 'zod';
|
||||
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;
|
||||
import { z } from 'zod'
|
||||
import { userSchema } from './zod-schemas'
|
||||
|
||||
export const updateUserPasswordSchema = userSchema
|
||||
.pick({ password: true, confirm_password: true })
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
refinePasswords(confirm_password, password, ctx)
|
||||
})
|
||||
|
||||
export const refinePasswords = async function (
|
||||
confirm_password: string,
|
||||
password: string,
|
||||
ctx: z.RefinementCtx,
|
||||
) {
|
||||
comparePasswords(confirm_password, password, ctx);
|
||||
checkPasswordStrength(password, ctx);
|
||||
};
|
||||
export const refinePasswords = async function (confirm_password: string, password: string, ctx: z.RefinementCtx) {
|
||||
comparePasswords(confirm_password, password, ctx)
|
||||
checkPasswordStrength(password, ctx)
|
||||
}
|
||||
|
||||
const comparePasswords = async function (
|
||||
confirm_password: string,
|
||||
password: string,
|
||||
ctx: z.RefinementCtx,
|
||||
) {
|
||||
const comparePasswords = async function (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 function (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);
|
||||
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,
|
||||
countOfLowerCase = 0,
|
||||
countOfNumbers = 0,
|
||||
countOfSpecialChar = 0;
|
||||
countOfSpecialChar = 0
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
const char = password.charAt(i);
|
||||
const char = password.charAt(i)
|
||||
if (!isNaN(+char)) {
|
||||
countOfNumbers++;
|
||||
countOfNumbers++
|
||||
} else if (containsUppercase(char)) {
|
||||
countOfUpperCase++;
|
||||
countOfUpperCase++
|
||||
} else if (containsLowercase(char)) {
|
||||
countOfLowerCase++;
|
||||
countOfLowerCase++
|
||||
} else if (containsSpecialChar(char)) {
|
||||
countOfSpecialChar++;
|
||||
countOfSpecialChar++
|
||||
}
|
||||
}
|
||||
|
||||
let errorMessage = 'Your password:';
|
||||
let errorMessage = 'Your password:'
|
||||
|
||||
if (countOfLowerCase < 1) {
|
||||
errorMessage = ' Must have at least one lowercase letter. ';
|
||||
errorMessage = ' Must have at least one lowercase letter. '
|
||||
}
|
||||
if (countOfNumbers < 1) {
|
||||
errorMessage += ' Must have at least one number. ';
|
||||
errorMessage += ' Must have at least one number. '
|
||||
}
|
||||
if (countOfUpperCase < 1) {
|
||||
errorMessage += ' Must have at least one uppercase letter. ';
|
||||
errorMessage += ' Must have at least one uppercase letter. '
|
||||
}
|
||||
if (countOfSpecialChar < 1) {
|
||||
errorMessage += ' Must have at least one special character.';
|
||||
errorMessage += ' Must have at least one special character.'
|
||||
}
|
||||
if (minimumLength) {
|
||||
errorMessage += ' Be at least 8 characters long.';
|
||||
errorMessage += ' Be at least 8 characters long.'
|
||||
}
|
||||
if (maximumLength) {
|
||||
errorMessage += ' Be less than 128 characters long.';
|
||||
errorMessage += ' Be less than 128 characters long.'
|
||||
}
|
||||
|
||||
if (errorMessage.length > 'Your password:'.length) {
|
||||
|
|
@ -114,14 +71,14 @@ const checkPasswordStrength = async function (password: string, ctx: z.Refinemen
|
|||
code: 'custom',
|
||||
message: errorMessage,
|
||||
path: ['password'],
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const addRoleSchema = z.object({
|
||||
roles: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: 'You have to select at least one item.',
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
||||
export type AddRoleSchema = typeof addRoleSchema;
|
||||
export type AddRoleSchema = typeof addRoleSchema
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import { superValidate } from 'sveltekit-superforms/server'
|
|||
import { collection_items, collections, gamesTable } from '../../../../lib/server/api/databases/tables'
|
||||
|
||||
export async function load(event) {
|
||||
const { user, session } = event.locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event)
|
||||
const { locals } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -22,7 +24,7 @@ export async function load(event) {
|
|||
name: true,
|
||||
created_at: true,
|
||||
},
|
||||
where: eq(collections.user_id, user!.id!),
|
||||
where: eq(collections.user_id, authedUser.id),
|
||||
})
|
||||
console.log('collections', userCollections)
|
||||
|
||||
|
|
@ -46,14 +48,17 @@ export async function load(event) {
|
|||
export const actions: Actions = {
|
||||
// Add game to a wishlist
|
||||
add: async (event) => {
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
const { locals } = event
|
||||
|
||||
if (!event.locals.user) {
|
||||
throw fail(401)
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
|
|
@ -108,13 +113,15 @@ export const actions: Actions = {
|
|||
// Remove game from a wishlist
|
||||
remove: async (event) => {
|
||||
const { locals } = event
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
if (!locals.user) {
|
||||
throw fail(401)
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
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),
|
||||
})
|
||||
|
||||
|
|
@ -125,7 +132,7 @@ export const actions: Actions = {
|
|||
|
||||
try {
|
||||
const collection = await db.query.collections.findFirst({
|
||||
where: eq(collections.user_id, locals.user.id),
|
||||
where: eq(collections.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
if (!collection) {
|
||||
|
|
|
|||
|
|
@ -131,14 +131,15 @@ export const actions: Actions = {
|
|||
// Add game to a wishlist
|
||||
add: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
const game = await db.query.games.findFirst({
|
||||
const game = await db.query.gamesTable.findFirst({
|
||||
where: eq(gamesTable.id, form.data.id),
|
||||
})
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ export const actions: Actions = {
|
|||
|
||||
try {
|
||||
const collection = await db.query.collections.findFirst({
|
||||
where: eq(collections.user_id, user!.id!),
|
||||
where: eq(collections.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
if (!collection) {
|
||||
|
|
@ -179,31 +180,35 @@ export const actions: Actions = {
|
|||
// Create new wishlist
|
||||
create: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
// Delete a wishlist
|
||||
delete: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
// Remove game from a wishlist
|
||||
remove: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
const game = await db.query.games.findFirst({
|
||||
const game = await db.query.gamesTable.findFirst({
|
||||
where: eq(gamesTable.id, form.data.id),
|
||||
})
|
||||
|
||||
|
|
@ -214,7 +219,7 @@ export const actions: Actions = {
|
|||
|
||||
try {
|
||||
const collection = await db.query.collections.findFirst({
|
||||
where: eq(collections.user_id, user!.id!),
|
||||
where: eq(collections.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
if (!collection) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
|
||||
export async function load(event) {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
const { locals } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import type { PageServerLoad } from '../$types';
|
||||
import { BggForm } from '$lib/zodValidation';
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils'
|
||||
import { BggForm } from '$lib/zodValidation'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { zod } from 'sveltekit-superforms/adapters'
|
||||
import { superValidate } from 'sveltekit-superforms/server'
|
||||
import type { PageServerLoad } from '../$types'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
const { locals } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const form = await superValidate({}, zod(BggForm));
|
||||
const form = await superValidate({}, zod(BggForm))
|
||||
|
||||
return { form };
|
||||
};
|
||||
return { form }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,11 @@ export async function load(event) {
|
|||
export const actions: Actions = {
|
||||
// Add game to a wishlist
|
||||
add: async (event) => {
|
||||
const { params, locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
const { locals, params } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
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),
|
||||
})
|
||||
|
||||
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
|
@ -103,25 +104,28 @@ export const actions: Actions = {
|
|||
// Create new wishlist
|
||||
create: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
},
|
||||
// Delete a wishlist
|
||||
delete: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
},
|
||||
// Remove game from a wishlist
|
||||
remove: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export const load = async () => {
|
||||
return {}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
15
src/routes/(app)/(protected)/settings/+layout.svelte
Normal file
15
src/routes/(app)/(protected)/settings/+layout.svelte
Normal 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>
|
||||
8
src/routes/(app)/(protected)/settings/+page.server.ts
Normal file
8
src/routes/(app)/(protected)/settings/+page.server.ts
Normal 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')
|
||||
}
|
||||
|
|
@ -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 { usersTable } from '$lib/server/api/databases/tables'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { changeEmailSchema, profileSchema } from '$lib/validations/account'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
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 { z } from 'zod'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import { updateEmailSchema, updateProfileSchema } from './schemas'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event
|
||||
|
|
@ -28,14 +26,14 @@ export const load: PageServerLoad = async (event) => {
|
|||
// where: eq(usersTable.id, user!.id!),
|
||||
// });
|
||||
|
||||
const profileForm = await superValidate(zod(profileSchema), {
|
||||
const profileForm = await superValidate(zod(updateProfileSchema), {
|
||||
defaults: {
|
||||
firstName: authedUser?.firstName ?? '',
|
||||
lastName: authedUser?.lastName ?? '',
|
||||
username: authedUser?.username ?? '',
|
||||
},
|
||||
})
|
||||
const emailForm = await superValidate(zod(changeEmailSchema), {
|
||||
const emailForm = await superValidate(zod(updateEmailSchema), {
|
||||
defaults: {
|
||||
email: authedUser?.email ?? '',
|
||||
},
|
||||
|
|
@ -66,7 +64,7 @@ export const actions: Actions = {
|
|||
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)
|
||||
console.log('data from profile update', error)
|
||||
|
|
@ -84,7 +82,7 @@ export const actions: Actions = {
|
|||
return message(form, { type: 'success', message: 'Profile updated successfully!' })
|
||||
},
|
||||
changeEmail: async (event) => {
|
||||
const form = await superValidate(event, zod(updateEmailDto))
|
||||
const form = await superValidate(event, zod(updateEmailSchema))
|
||||
|
||||
const newEmail = form.data?.email
|
||||
if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) {
|
||||
|
|
@ -1,27 +1,24 @@
|
|||
<script lang="ts">
|
||||
import * as Alert from '$components/ui/alert'
|
||||
import { Button } from '$components/ui/button'
|
||||
import { Input } from '$components/ui/input'
|
||||
import * as Alert from '$lib/components/ui/alert'
|
||||
// import * as Form from '$lib/components/ui/form';
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import { updateEmailDto } from '$lib/dtos/update-email.dto'
|
||||
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
|
||||
import { Label } from '$components/ui/label'
|
||||
import { AlertTriangle, KeyRound } from 'lucide-svelte'
|
||||
import * as flashModule from 'sveltekit-flash-message/client'
|
||||
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||
import { superForm } from 'sveltekit-superforms/client'
|
||||
import { updateEmailSchema, updateProfileSchema } from './schemas'
|
||||
|
||||
const { data } = $props()
|
||||
|
||||
const hasSetupTwoFactor = data.hasSetupTwoFactor
|
||||
|
||||
const {
|
||||
form: profileForm,
|
||||
errors: profileErrors,
|
||||
enhance: profileEnhance,
|
||||
} = superForm(data.profileForm, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(updateProfileDto),
|
||||
validators: zodClient(updateProfileSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
syncFlashMessage: true,
|
||||
|
|
@ -36,7 +33,7 @@ const {
|
|||
enhance: emailEnhance,
|
||||
} = superForm(data.emailForm, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(updateEmailDto),
|
||||
validators: zodClient(updateEmailSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
syncFlashMessage: true,
|
||||
|
|
@ -101,27 +98,6 @@ const {
|
|||
{/if}
|
||||
</div>
|
||||
</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">
|
||||
form {
|
||||
20
src/routes/(app)/(protected)/settings/profile/schemas.ts
Normal file
20
src/routes/(app)/(protected)/settings/profile/schemas.ts
Normal 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>
|
||||
|
|
@ -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 = {}
|
||||
30
src/routes/(app)/(protected)/settings/security/+page.svelte
Normal file
30
src/routes/(app)/(protected)/settings/security/+page.svelte
Normal 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>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { usersTable } from '$lib/server/api/databases/tables'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { changeUserPasswordSchema } from '$lib/validations/account'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { Cookie } from 'lucia'
|
||||
|
|
@ -8,8 +8,8 @@ import { Argon2id } from 'oslo/password'
|
|||
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'
|
||||
import { usersTable } from '../../../../../../../lib/server/api/databases/tables'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import { changeUserPasswordSchema } from './schemas'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
import { superForm } from 'sveltekit-superforms/client';
|
||||
import { AlertTriangle } from 'lucide-svelte';
|
||||
import * as Alert from "$components/ui/alert";
|
||||
import * as Form from '$components/ui/form';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { changeUserPasswordSchema } from '$lib/validations/account';
|
||||
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 { changeUserPasswordSchema } from './schemas'
|
||||
|
||||
export let data;
|
||||
const { data } = $props()
|
||||
|
||||
const form = superForm(data.form, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(changeUserPasswordSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent'
|
||||
});
|
||||
const form = superForm(data.form, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(changeUserPasswordSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
})
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
const { form: formData, enhance } = form
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
|
|
@ -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'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account'
|
||||
import env from '$src/env'
|
||||
import env from '$lib/server/api/common/env'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import kebabCase from 'just-kebab-case'
|
||||
import { base32, decodeHex } from 'oslo/encoding'
|
||||
|
|
@ -10,6 +9,7 @@ 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'
|
||||
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event
|
||||
|
|
@ -125,7 +125,7 @@ export const actions: Actions = {
|
|||
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) => {
|
||||
const { locals } = event
|
||||
|
|
@ -162,7 +162,7 @@ export const actions: Actions = {
|
|||
|
||||
redirect(
|
||||
302,
|
||||
'/profile/security/mfa',
|
||||
'/settings/security/mfa',
|
||||
{
|
||||
type: 'success',
|
||||
message: 'Two-Factor Authentication has been disabled.',
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -90,18 +90,20 @@ export const actions: Actions = {
|
|||
// Create new wishlist
|
||||
create: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
// Delete a wishlist
|
||||
delete: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
|
|
@ -116,7 +118,7 @@ export const actions: Actions = {
|
|||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
try {
|
||||
const game = await db.query.games.findFirst({
|
||||
const game = await db.query.gamesTable.findFirst({
|
||||
where: eq(gamesTable.id, form.data.id),
|
||||
})
|
||||
|
||||
|
|
@ -131,7 +133,7 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
if (game) {
|
||||
const wishlist = await db.query.wishlists.findFirst({
|
||||
const wishlist = await db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -51,14 +51,15 @@ export const actions: Actions = {
|
|||
// Add game to a wishlist
|
||||
add: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
try {
|
||||
const game = await db.query.games.findFirst({
|
||||
const game = await db.query.gamesTable.findFirst({
|
||||
where: eq(gamesTable.id, form.data.id),
|
||||
})
|
||||
|
||||
|
|
@ -73,8 +74,8 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
if (game) {
|
||||
const wishlist = await db.query.wishlists.findFirst({
|
||||
where: eq(wishlistsTable.user_id, user!.id!),
|
||||
const wishlist = await db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
if (!wishlist) {
|
||||
|
|
@ -99,32 +100,35 @@ export const actions: Actions = {
|
|||
// Create new wishlist
|
||||
create: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
// Delete a wishlist
|
||||
delete: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
return error(405, 'Method not allowed')
|
||||
},
|
||||
// Remove game from a wishlist
|
||||
remove: async (event) => {
|
||||
const { locals } = event
|
||||
const { user, session } = locals
|
||||
if (userNotAuthenticated(user, session)) {
|
||||
return fail(401)
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
const form = await superValidate(event, zod(modifyListGameSchema))
|
||||
|
||||
try {
|
||||
const game = await db.query.games.findFirst({
|
||||
const game = await db.query.gamesTable.findFirst({
|
||||
where: eq(gamesTable.id, form.data.id),
|
||||
})
|
||||
|
||||
|
|
@ -139,8 +143,8 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
if (game) {
|
||||
const wishlist = await db.query.wishlists.findFirst({
|
||||
where: eq(wishlistsTable.user_id, user!.id!),
|
||||
const wishlist = await db.query.wishlistsTable.findFirst({
|
||||
where: eq(wishlistsTable.user_id, authedUser.id),
|
||||
})
|
||||
|
||||
if (!wishlist) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ export const actions: Actions = {
|
|||
const form = await superValidate(event, zod(signinUsernameDto))
|
||||
|
||||
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) {
|
||||
form.data.password = ''
|
||||
|
|
|
|||
|
|
@ -1,57 +1,56 @@
|
|||
import { fail, error, type Actions } from '@sveltejs/kit';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {resetPasswordEmailSchema, resetPasswordTokenSchema} from "$lib/validations/auth";
|
||||
import {StatusCodes} from "$lib/constants/status-codes";
|
||||
import {userFullyAuthenticated} from "$lib/server/auth-utils";
|
||||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { resetPasswordEmailSchema, resetPasswordTokenSchema } from '$lib/validations/auth'
|
||||
import { type Actions, error, fail } from '@sveltejs/kit'
|
||||
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 () => {
|
||||
return {
|
||||
emailForm: await superValidate(zod(resetPasswordEmailSchema)),
|
||||
tokenForm: await superValidate(zod(resetPasswordTokenSchema)),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
passwordReset: async (event) => {
|
||||
const { request, locals } = event;
|
||||
const { user, session } = locals;
|
||||
const { request, locals } = event
|
||||
|
||||
if (userFullyAuthenticated(user, session)) {
|
||||
const message = { type: 'success', message: 'You are already signed in' } as const;
|
||||
throw redirect('/', message, event);
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const emailForm = await superValidate(request, zod(resetPasswordEmailSchema));
|
||||
const emailForm = await superValidate(request, zod(resetPasswordEmailSchema))
|
||||
if (!emailForm.valid) {
|
||||
return fail(StatusCodes.BAD_REQUEST, { emailForm });
|
||||
return fail(StatusCodes.BAD_REQUEST, { emailForm })
|
||||
}
|
||||
// const error = {};
|
||||
// // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
||||
// if (error) {
|
||||
// return setError(emailForm, 'email', error);
|
||||
// }
|
||||
return { emailForm };
|
||||
return { emailForm }
|
||||
},
|
||||
verifyToken: async (event) => {
|
||||
const { request, locals } = event;
|
||||
const { user, session } = locals;
|
||||
if (userFullyAuthenticated(user, session)) {
|
||||
const message = { type: 'success', message: 'You are already signed in' } as const;
|
||||
throw redirect('/', message, event);
|
||||
const { request, locals } = event
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema));
|
||||
const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema))
|
||||
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)
|
||||
if (error) {
|
||||
return setError(tokenForm, 'token', error);
|
||||
return setError(tokenForm, 'token', error)
|
||||
}
|
||||
redirect(301, '/');
|
||||
}
|
||||
};
|
||||
redirect(301, '/')
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import env from '$lib/server/api/common/env'
|
||||
import { twoFactorTable, usersTable } from '$lib/server/api/databases/tables'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
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 { zod } from 'sveltekit-superforms/adapters'
|
||||
import { superValidate } from 'sveltekit-superforms/server'
|
||||
import env from '../../../env'
|
||||
import type { PageServerLoad, RequestEvent } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const config = {
|
|||
$db: './src/lib/server/api/infrastructure/database',
|
||||
$server: './src/server',
|
||||
$lib: './src/lib',
|
||||
$routes: './src/routes',
|
||||
$src: './src',
|
||||
$state: './src/state',
|
||||
$styles: './src/styles',
|
||||
|
|
|
|||
Loading…
Reference in a new issue