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

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

View file

@ -1,14 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"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
}
}

View file

@ -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',

View file

@ -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",

View file

@ -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

View file

@ -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">

View file

@ -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;
}
}

View file

@ -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;

View file

@ -1,22 +1,21 @@
import { tv, type VariantProps } from "tailwind-variants";
import { type VariantProps, tv } from "tailwind-variants";
export { default as Badge } from "./badge.svelte";
export 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"];

View file

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

View file

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

View file

@ -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
}

View file

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

View file

@ -49,8 +49,9 @@ export class MfaController extends Controller {
const user = c.var.user
// 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 })

View file

@ -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',

View file

@ -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'

View file

@ -2,7 +2,7 @@ import * as schema from '$lib/server/api/databases/tables'
import type { db } from '$lib/server/api/packages/drizzle'
import { 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()

View file

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

View file

@ -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' }))

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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,

View file

@ -1,4 +1,4 @@
import { config } from '$lib/server/api/configs/config'
import { config } from '$lib/server/api/common/config'
import * as schema from '$lib/server/api/databases/tables'
import { 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',
})
}

View file

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

View file

@ -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[]

View file

@ -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

View file

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

View file

@ -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'>

View file

@ -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,
};
};
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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) {

View file

@ -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 {}
}

View file

@ -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 }
}

View file

@ -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)
}
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,6 @@
import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { notSignedInMessage } from '$lib/flashMessages'
import { 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)) {

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { 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

View file

@ -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>

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { 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.',

View file

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

View file

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

View file

@ -90,18 +90,20 @@ export const actions: Actions = {
// Create new wishlist
create: 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),
})

View file

@ -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) {

View file

@ -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 = ''

View file

@ -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, '/')
},
}

View file

@ -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) => {

View file

@ -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',