Cleanup code, fix IntelliJ removing star imports, move sessions to Redis.

This commit is contained in:
Bradley Shellnut 2024-11-09 11:05:28 -08:00
parent 88339093e1
commit c9b6269ce9
124 changed files with 1343 additions and 1342 deletions

View file

@ -40,14 +40,17 @@
"@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"drizzle-kit": "^0.27.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13",
"formsnap": "^1.0.1",
"just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0",
"mode-watcher": "^0.4.1",
"nodemailer": "^6.9.16",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
@ -61,6 +64,7 @@
"svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"svelte-sonner": "^0.3.28",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.20.0",
"tailwindcss": "^3.4.14",
@ -97,7 +101,6 @@
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1",
"bullmq": "^5.25.3",
"class-variance-authority": "^0.7.0",
@ -108,7 +111,6 @@
"drizzle-orm": "^0.36.1",
"drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8",
"hono": "^4.6.9",
"hono-pino": "^0.3.0",
@ -120,7 +122,6 @@
"just-capitalize": "^3.2.0",
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"mode-watcher": "^0.4.1",
"open-props": "^1.7.7",
"oslo": "^1.2.1",
"pg": "^8.13.1",
@ -133,7 +134,6 @@
"reflect-metadata": "^0.2.2",
"stoker": "^1.3.0",
"svelte-lazy-loader": "^1.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7",

View file

@ -77,9 +77,6 @@ importers:
'@types/feather-icons':
specifier: ^4.29.4
version: 4.29.4
bits-ui:
specifier: ^0.21.16
version: 0.21.16(svelte@5.0.0-next.175)
boardgamegeekclient:
specifier: ^1.9.1
version: 1.9.1
@ -110,9 +107,6 @@ importers:
feather-icons:
specifier: ^4.29.2
version: 4.29.2
formsnap:
specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
handlebars:
specifier: ^4.7.8
version: 4.7.8
@ -146,9 +140,6 @@ importers:
loader:
specifier: ^2.1.1
version: 2.1.1
mode-watcher:
specifier: ^0.4.1
version: 0.4.1(svelte@5.0.0-next.175)
open-props:
specifier: ^1.7.7
version: 1.7.7
@ -185,9 +176,6 @@ importers:
svelte-lazy-loader:
specifier: ^1.0.0
version: 1.0.0
svelte-sonner:
specifier: ^0.3.28
version: 0.3.28(svelte@5.0.0-next.175)
tailwind-merge:
specifier: ^2.5.4
version: 2.5.4
@ -255,6 +243,9 @@ importers:
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47)
bits-ui:
specifier: ^0.21.16
version: 0.21.16(svelte@5.0.0-next.175)
drizzle-kit:
specifier: ^0.27.2
version: 0.27.2
@ -267,6 +258,9 @@ importers:
eslint-plugin-svelte:
specifier: 2.36.0-next.13
version: 2.36.0-next.13(eslint@8.57.1)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
formsnap:
specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
just-clone:
specifier: ^6.2.0
version: 6.2.0
@ -279,6 +273,9 @@ importers:
lucide-svelte:
specifier: ^0.408.0
version: 0.408.0(svelte@5.0.0-next.175)
mode-watcher:
specifier: ^0.4.1
version: 0.4.1(svelte@5.0.0-next.175)
nodemailer:
specifier: ^6.9.16
version: 6.9.16
@ -318,6 +315,9 @@ importers:
svelte-sequential-preprocessor:
specifier: ^2.0.2
version: 2.0.2
svelte-sonner:
specifier: ^0.3.28
version: 0.3.28(svelte@5.0.0-next.175)
sveltekit-flash-message:
specifier: ^2.4.4
version: 2.4.4(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)
@ -3834,8 +3834,8 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-inspect@1.13.2:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
object-inspect@1.13.3:
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'}
obuf@1.1.2:
@ -8339,7 +8339,7 @@ snapshots:
object-hash@3.0.0: {}
object-inspect@1.13.2: {}
object-inspect@1.13.3: {}
obuf@1.1.2: {}
@ -9122,7 +9122,7 @@ snapshots:
call-bind: 1.0.7
es-errors: 1.3.0
get-intrinsic: 1.2.4
object-inspect: 1.13.2
object-inspect: 1.13.3
siginfo@2.0.0: {}

10
src/app.d.ts vendored
View file

@ -1,6 +1,6 @@
import type { User } from 'lucia';
import type { ApiClient } from '$lib/server/api';
import type { parseApiResponse } from '$lib/utils/api';
import type { User } from 'lucia';
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
@ -24,9 +24,9 @@ declare global {
}
namespace Superforms {
type Message = {
type: 'error' | 'success' | 'info',
text: string
}
type: 'error' | 'success' | 'info';
text: string;
};
}
interface Error {
code?: string;
@ -37,7 +37,7 @@ declare global {
interface Document {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
startViewTransition: (callback: never) => void; // Add your custom property/method here
startViewTransition: (callback: never) => void; // Add your custom property/method here
}
}

View file

@ -1,16 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="robots" content="noindex, nofollow" />
<meta charset="utf-8" />
<meta name="description" content="Bored? Find a game! Bored Game!" />
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg" />
<meta name="viewport" content="width=device-width" />
<head>
<meta name="robots" content="noindex, nofollow"/>
<meta charset="utf-8"/>
<meta name="description" content="Bored? Find a game! Bored Game!"/>
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg"/>
<meta name="viewport" content="width=device-width"/>
%sveltekit.head%
</head>
%sveltekit.head%
</head>
<body>
<div id="svelte">%sveltekit.body%</div>
</body>
<body>
<div id="svelte">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,10 +1,10 @@
import 'reflect-metadata'
import { StatusCodes } from '$lib/constants/status-codes'
import type { ApiRoutes } from '$lib/server/api'
import { parseApiResponse } from '$lib/utils/api'
import { type Handle, redirect } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { hc } from 'hono/client'
import 'reflect-metadata';
import { StatusCodes } from '$lib/constants/status-codes';
import type { ApiRoutes } from '$lib/server/api';
import { parseApiResponse } from '$lib/utils/api';
import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { hc } from 'hono/client';
const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */
@ -14,29 +14,30 @@ const apiClient: Handle = async ({ event, resolve }) => {
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
host: event.request.headers.get('host') || '',
},
})
});
/* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse)
return data?.user
const { data } = await api.user.$get().then(parseApiResponse);
return data?.user;
}
async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse)
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
return data?.user
const { data } = await api.user.$get().then(parseApiResponse);
if (!data || !data.user) {
throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
}
return data?.user;
}
/* ------------------------------ Set contexts ------------------------------ */
event.locals.api = api
event.locals.parseApiResponse = parseApiResponse
event.locals.getAuthedUser = getAuthedUser
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow
event.locals.api = api;
event.locals.parseApiResponse = parseApiResponse;
event.locals.getAuthedUser = getAuthedUser;
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
/* ----------------------------- Return response ---------------------------- */
const response = await resolve(event)
return response
}
return await resolve(event);
};
export const handle: Handle = sequence(apiClient)
export const handle: Handle = sequence(apiClient);

View file

@ -1,71 +1,71 @@
<script>
/**
* @event {null} save
* @event {{ prevValue: any; value: any; }} update
*/
/**
* @event {null} save
* @event {{ prevValue: any; value: any; }} update
*/
/**
* Specify the local storage key
*/
export let key = 'local-storage-key';
/**
* Specify the local storage key
*/
export let key = 'local-storage-key';
/**
* Provide a value to persist
* @type {any}
*/
export let value = '';
/**
* Provide a value to persist
* @type {any}
*/
export let value = '';
/**
* Remove the persisted key value from the browser's local storage
* @type {() => void}
*/
export function clearItem() {
localStorage.removeItem(key);
}
/**
* Remove the persisted key value from the browser's local storage
* @type {() => void}
*/
export function clearItem() {
localStorage.removeItem(key);
}
/**
* Clear all key values from the browser's local storage
* @type {() => void}
*/
export function clearAll() {
localStorage.clear();
}
/**
* Clear all key values from the browser's local storage
* @type {() => void}
*/
export function clearAll() {
localStorage.clear();
}
import { onMount, afterUpdate, createEventDispatcher } from 'svelte';
import { afterUpdate, createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
let prevValue = value;
let prevValue = value;
function setItem() {
if (typeof value === 'object') {
localStorage.setItem(key, JSON.stringify(value));
} else {
localStorage.setItem(key, value);
}
}
function setItem() {
if (typeof value === 'object') {
localStorage.setItem(key, JSON.stringify(value));
} else {
localStorage.setItem(key, value);
}
}
onMount(() => {
const item = localStorage.getItem(key);
onMount(() => {
const item = localStorage.getItem(key);
if (item != null) {
try {
value = JSON.parse(item);
} catch (e) {
value = item;
}
} else {
setItem(value);
dispatch('save');
}
});
if (item != null) {
try {
value = JSON.parse(item);
} catch (e) {
value = item;
}
} else {
setItem(value);
dispatch('save');
}
});
afterUpdate(() => {
if (prevValue !== value) {
setItem(value);
dispatch('update', { prevValue, value });
}
afterUpdate(() => {
if (prevValue !== value) {
setItem(value);
dispatch('update', { prevValue, value });
}
prevValue = value;
});
prevValue = value;
});
</script>

View file

@ -1,14 +1,15 @@
<script lang="ts">
import type { GameType, SavedGameType } from '$lib/types';
import type { CollectionItems } from '$db/schema';
import type { CollectionItems } from '$db/schema';
import * as Card from '$lib/components/ui/card';
import type { GameType, SavedGameType } from '$lib/types';
export let game: GameType | CollectionItems;
export let variant: 'default' | 'compact' = 'default';
export let game: GameType | CollectionItems;
export let variant: 'default' | 'compact' = 'default';
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
</script>
<article class="grid grid-template-cols-2 gap-4">

View file

@ -1,10 +1,12 @@
<script lang="ts">
import Logo from '$components/logo.svelte';
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
import Logo from '$components/logo.svelte';
import * as Avatar from '$lib/components/ui/avatar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
let { user = null } = $props();
let { user = null } = $props();
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
</script>
<header>

View file

@ -1,19 +1,20 @@
<script lang="ts">
import { cn } from '$lib/utils/ui';
import { type PinInputProps } from 'bits-ui';
import { cn } from '$lib/utils/ui';
import { PinInput } from 'bits-ui';
import { type PinInputProps } from 'bits-ui';
interface Props extends Omit<PinInputProps, 'value'> {
value: string;
inputCount?: number;
}
interface Props extends Omit<PinInputProps, 'value'> {
value: string;
inputCount?: number;
}
let { value = $bindable(), inputCount = 6, ...rest }: Props = $props();
let pin = $state<string[] | undefined>(value?.split('') ?? []);
let inputs = $derived(Array(inputCount).fill(null));
let { value = $bindable(), inputCount = 6, ...rest }: Props = $props();
let pin = $state<string[] | undefined>(value?.split('') ?? []);
let inputs = $derived(Array(inputCount).fill(null));
$effect(() => {
value = pin?.join('') ?? '';
});
$effect(() => {
value = pin?.join('') ?? '';
});
</script>
<PinInput.Root

View file

@ -1,17 +1,18 @@
<script lang="ts">
import { type Infer, superForm, type SuperValidated } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { search_schema, type SearchSchema } from '$lib/zodValidation';
import Input from '$components/ui/input/input.svelte';
import Checkbox from '$components/ui/checkbox/checkbox.svelte';
import Checkbox from '$components/ui/checkbox/checkbox.svelte';
import * as Form from '$components/ui/form';
import Input from '$components/ui/input/input.svelte';
import { type SearchSchema, search_schema } from '$lib/zodValidation';
import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
export let data: SuperValidated<Infer<SearchSchema>>;
export let data: SuperValidated<Infer<SearchSchema>>;
const form = superForm(data, {
validators: zodClient(search_schema),
});
const form = superForm(data, {
validators: zodClient(search_schema),
});
const { form: formData } = form;
const { form: formData } = form;
</script>
<search>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
import type { HeadingLevel } from '.';
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
level?: HeadingLevel;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
import { alertVariants, type Variant } from '.';
import type { HTMLAttributes } from "svelte/elements";
import { type Variant, alertVariants } from "./index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement> & {
variant?: Variant;
@ -12,10 +12,6 @@
export { className as class };
</script>
<div
class={cn(alertVariants({ variant }), className)}
{...$$restProps}
role="alert"
>
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
<slot />
</div>

View file

@ -1,22 +1,22 @@
import {tv, type VariantProps} from "tailwind-variants";
import { type VariantProps, tv } from "tailwind-variants";
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export const alertVariants = tv({
base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4",
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive"
}
"border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default"
}
variant: "default",
},
});
export type Variant = VariantProps<typeof alertVariants>["variant"];
@ -29,5 +29,5 @@ export {
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle
Title as AlertTitle,
};

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { buttonVariants, type Events, type Props } from './index.js';
import { cn } from '$lib/utils.js';
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = Props;
type $$Events = Events;

View file

@ -1,5 +1,5 @@
import {tv, type VariantProps} from "tailwind-variants";
import type {Button as ButtonPrimitive} from "bits-ui";
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import type { HeadingLevel } from './index.js';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/ui.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Check from 'lucide-svelte/icons/check';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
@ -14,7 +14,7 @@
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
@ -17,7 +17,7 @@
{transitionConfig}
{sideOffset}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
className
)}
{...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
@ -14,7 +14,7 @@
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Circle from 'lucide-svelte/icons/circle';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
@ -13,7 +13,7 @@
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
@ -9,6 +9,6 @@
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...$$restProps}
/>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
@ -18,7 +18,7 @@
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...$$restProps}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
@ -15,7 +15,7 @@
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}

View file

@ -1,4 +1,4 @@
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button/index.js';
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;

View file

@ -1,6 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import * as FormPrimitive from "formsnap";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;

View file

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;

View file

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,13 +1,12 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { cn } from "$lib/utils/ui.js";
import { Label } from "$lib/components/ui/label/index.js";
type $$Props = LabelPrimitive.Props;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.LegendProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLUListElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLLIElement>;
let className: $$Props["class"] = undefined;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { buttonVariants, type Props } from '$lib/components/ui/button/index.js';
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
import { type Props, buttonVariants } from "$lib/components/ui/button/index.js";
type $$Props = PaginationPrimitive.PageProps &
Props & {

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { Button } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.NextButtonProps;
type $$Events = PaginationPrimitive.NextButtonEvents;

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import ChevronLeft from 'lucide-svelte/icons/chevron-left';
import { Button } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.PrevButtonProps;
type $$Events = PaginationPrimitive.PrevButtonEvents;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js';
import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.Props;
type $$Events = PaginationPrimitive.Events;
@ -11,7 +11,6 @@
export let perPage: $$Props["perPage"] = 10;
export let page: $$Props["page"] = 1;
export let siblingCount: $$Props["siblingCount"] = 1;
export { className as class };
$: currentPage = page;

View file

@ -1,4 +1,4 @@
import {Select as SelectPrimitive} from "bits-ui";
import { Select as SelectPrimitive } from "bits-ui";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { scale } from 'svelte/transition';
import { cn, flyAndScale } from '$lib/utils.js';
import { Select as SelectPrimitive } from "bits-ui";
import { scale } from "svelte/transition";
import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents;
@ -27,7 +27,7 @@
{outTransitionConfig}
{sideOffset}
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
"bg-popover text-popover-foreground relative z-50 min-w-[8rem] overflow-hidden rounded-md border shadow-md outline-none",
className
)}
{...$$restProps}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import Check from 'lucide-svelte/icons/check';
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import Check from "lucide-svelte/icons/check";
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
@ -18,7 +18,7 @@
{disabled}
{label}
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.LabelProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.SeparatorProps;
@ -8,4 +8,4 @@
export { className as class };
</script>
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import ChevronDown from 'lucide-svelte/icons/chevron-down';
import { cn } from '$lib/utils.js';
import { Select as SelectPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
@ -12,7 +12,7 @@
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"border-input bg-background ring-offset-background focus-visible:ring-ring aria-[invalid]:border-destructive data-[placeholder]:[&>span]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from 'mode-watcher';
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
type $$Props = SonnerProps;
</script>

View file

@ -1,37 +1,37 @@
<script lang="ts">
import { onNavigate } from '$app/navigation'
import { navigating } from '$app/stores'
import { onNavigate } from '$app/navigation';
import { navigating } from '$app/stores';
let visible = false
let progress = 0
let load_durations: number[] = []
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length
const increment = 1
let visible = false;
let progress = 0;
let load_durations: number[] = [];
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length;
const increment = 1;
onNavigate((navigation) => {
const typical_load_time = average_load || 200 //ms
const frequency = typical_load_time / 100
let start = performance.now()
const typical_load_time = average_load || 200; //ms
const frequency = typical_load_time / 100;
let start = performance.now();
// Start the progress bar
visible = true
progress = 0
visible = true;
progress = 0;
const interval = setInterval(() => {
// Increment the progress bar
progress += increment
}, frequency)
progress += increment;
}, frequency);
// Resolve the promise when the page is done loading
$navigating?.complete.then(() => {
progress = 100 // Fill out the progress bar
clearInterval(interval)
progress = 100; // Fill out the progress bar
clearInterval(interval);
// after 100 ms hide the progress bar
setTimeout(() => {
visible = false
}, 500)
visible = false;
}, 500);
// Log how long that one took
const end = performance.now()
const duration = end - start
load_durations = [...load_durations, duration]
})
})
const end = performance.now();
const duration = end - start;
load_durations = [...load_durations, duration];
});
});
</script>
<div class="progress" class:visible style:--progress={progress}>

View file

@ -1,19 +1,39 @@
import {
cookieExpiresAt,
cookieExpiresMilliseconds,
halfCookieExpiresMilliseconds
} from '$lib/server/api/common/utils/cookies';
import {SessionsRepository} from '$lib/server/api/repositories/sessions.repository';
import {sha256} from '@oslojs/crypto/sha2';
import {encodeBase32LowerCaseNoPadding, encodeHexLowerCase} from '@oslojs/encoding';
import {inject, injectable} from 'tsyringe';
import type {Sessions, Users} from '../databases/postgres/tables';
import { cookieExpiresAt, cookieExpiresMilliseconds, halfCookieExpiresMilliseconds } from '$lib/server/api/common/utils/cookies';
import { UsersRepository } from '$lib/server/api/repositories/users.repository';
import { RedisService } from '$lib/server/api/services/redis.service';
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { inject, injectable } from 'tsyringe';
import type { Users } from '../databases/postgres/tables';
export type SessionValidationResult = { session: Sessions; user: Users } | { session: null; user: null };
export type RedisSession = {
id: string;
user_id: string;
expires_at: number;
ip_country: string;
ip_address: string;
two_factor_auth_enabled: boolean;
is_two_factor_authenticated: boolean;
};
export type Session = {
id: string;
userId: string;
expiresAt: Date;
ipCountry: string;
ipAddress: string;
twoFactorAuthEnabled: boolean;
isTwoFactorAuthenticated: boolean;
};
export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined };
@injectable()
export class SessionsService {
constructor(@inject(SessionsRepository) private readonly sessionsRepository: SessionsRepository) {}
constructor(
@inject(RedisService) private readonly redisService: RedisService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
) {}
generateSessionToken() {
const bytes = new Uint8Array(20);
@ -30,7 +50,7 @@ export class SessionsService {
isTwoFactorAuthenticated: boolean,
) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Sessions = {
const session = {
id: sessionId,
userId,
expiresAt: cookieExpiresAt,
@ -39,23 +59,49 @@ export class SessionsService {
twoFactorAuthEnabled,
isTwoFactorAuthenticated,
};
await this.sessionsRepository.create(session);
await this.redisService.client.set(
`session:${sessionId}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: session.expiresAt,
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
return session;
}
async validateSessionToken(token: string): Promise<SessionValidationResult> {
// TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm
// const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessions = await this.sessionsRepository.findBySessionId(token);
if (sessions.length < 1) {
const item = await this.redisService.client.get(`session:${token}`);
if (item === null) {
return {
session: null,
user: null,
};
}
const { user, session } = sessions[0];
const result: RedisSession = JSON.parse(item);
const session: Session = {
id: result.id,
userId: result.user_id,
expiresAt: new Date(result.expires_at * 1000),
ipCountry: result.ip_country,
ipAddress: result.ip_address,
twoFactorAuthEnabled: result.two_factor_auth_enabled,
isTwoFactorAuthenticated: result.is_two_factor_authenticated,
};
let user: Users | undefined = undefined;
if (session.userId) {
user = await this.usersRepository.findOneById(session.userId);
}
if (Date.now() >= session.expiresAt.getTime()) {
await this.sessionsRepository.deleteBySessionId(token);
await this.redisService.client.del(`session:${token}`);
return {
session: null,
user: null,
@ -64,13 +110,26 @@ export class SessionsService {
if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) {
session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
await this.sessionsRepository.updateSessionExpiresAt(token, session.expiresAt);
await this.redisService.client.set(
`session:${token}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: Math.floor(session.expiresAt.getTime() / 1000),
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
}
return { session, user };
}
async invalidateSession(sessionId: string) {
await this.sessionsRepository.deleteBySessionId(sessionId);
await this.redisService.client.del(`session:${sessionId}`);
}
}

View file

@ -1,4 +1,4 @@
import { z, ZodNumber, ZodOptional } from 'zod';
import { ZodNumber, ZodOptional, z } from 'zod';
// import zodToJsonSchema from 'zod-to-json-schema';
export const BoardGameSearch = z.object({
@ -25,15 +25,7 @@ export type ListGameSchema = typeof list_game_request_schema;
// https://github.com/colinhacks/zod/discussions/330
export function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
return z.preprocess(
(value) =>
typeof value === 'string'
? Number.parseInt(value, 10)
: typeof value === 'number'
? value
: undefined,
schema,
);
return z.preprocess((value) => (typeof value === 'string' ? Number.parseInt(value, 10) : typeof value === 'number' ? value : undefined), schema);
}
const Search = z.object({
@ -73,47 +65,45 @@ export const search_schema = z
limit: z.number().min(10).max(100).default(10),
skip: z.number().min(0).default(0),
})
.superRefine(
({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
console.log({ minPlayers, maxPlayers });
if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['minPlayers'],
});
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['maxPlayers'],
});
}
.superRefine(({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
console.log({ minPlayers, maxPlayers });
if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['minPlayers'],
});
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['maxPlayers'],
});
}
if (exactMinAge && !minAge) {
ctx.addIssue({
code: 'custom',
message: 'Min Age required when searching for exact min age',
path: ['minAge'],
});
}
if (exactMinAge && !minAge) {
ctx.addIssue({
code: 'custom',
message: 'Min Age required when searching for exact min age',
path: ['minAge'],
});
}
if (exactMinPlayers && !minPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Min Players required when searching for exact min players',
path: ['minPlayers'],
});
}
if (exactMinPlayers && !minPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Min Players required when searching for exact min players',
path: ['minPlayers'],
});
}
if (exactMaxPlayers && !maxPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Max Players required when searching for exact max players',
path: ['maxPlayers'],
});
}
},
);
if (exactMaxPlayers && !maxPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Max Players required when searching for exact max players',
path: ['maxPlayers'],
});
}
});
export type SearchSchema = typeof search_schema;

View file

@ -1,6 +1,5 @@
import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages';
import { db } from '$lib/server/api/packages/drizzle';
import { errorMessage } from '$lib/utils/superforms';
import { eq } from 'drizzle-orm';
import { loadFlash, redirect } from 'sveltekit-flash-message/server';
import { user_roles } from '../../../../lib/server/api/databases/postgres/tables';

View file

@ -1,24 +1,22 @@
<script lang="ts">
import { getFlash } from 'sveltekit-flash-message';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { theme } from '$state/theme';
import { toastMessage } from '$lib/utils/superforms.js';
import { page } from '$app/stores';
import { theme } from '$state/theme';
import { getFlash } from 'sveltekit-flash-message';
const { data } = $props();
const { user } = data;
const { data } = $props();
const { user } = data;
const flash = getFlash(page, {
clearOnNavigate: true,
clearAfterMs: 3000,
clearArray: true
});
const flash = getFlash(page, {
clearOnNavigate: true,
clearAfterMs: 3000,
clearArray: true,
});
$effect(() => {
// set the theme to the user's active theme
$theme = user?.theme || 'system';
document.querySelector('html')?.setAttribute('data-theme', $theme);
});
$effect(() => {
// set the theme to the user's active theme
$theme = user?.theme || 'system';
document.querySelector('html')?.setAttribute('data-theme', $theme);
});
</script>
<h1>Do the admin stuff</h1>

View file

@ -1,13 +1,13 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { redirect } from 'sveltekit-flash-message/server'
import type { PageServerLoad } from './$types'
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 { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
// const users = await db.query.users.findMany({
@ -17,5 +17,5 @@ export const load: PageServerLoad = async (event) => {
return {
// users,
}
}
};
};

View file

@ -1,6 +1,7 @@
<script lang="ts">
import DataTable from './user-table.svelte';
const { data } = $props();
import DataTable from './user-table.svelte';
const { data } = $props();
</script>
<h1>Users</h1>

View file

@ -1,13 +1,13 @@
<script lang="ts">
import { enhance } from '$app/forms'
import { Button } from '$lib/components/ui/button'
import capitalize from 'just-capitalize'
import { enhance } from '$app/forms';
import { Button } from '$lib/components/ui/button';
import capitalize from 'just-capitalize';
// import AddRolesForm from './add-rolesTable-form.svelte';
const { data } = $props()
const { data } = $props();
const { user, availableRoles } = data
const { user_roles }: { user_roles: { role: { name: string; cuid: string } }[] } = user
const { user, availableRoles } = data;
const { user_roles }: { user_roles: { role: { name: string; cuid: string } }[] } = user;
</script>
<h1>User Details</h1>

View file

@ -1,13 +1,11 @@
<script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox/index.js'
import * as Form from '$lib/components/ui/form'
import { Input } from '$lib/components/ui/input'
import { type AddRoleSchema, addRoleSchema } from '$lib/validations/account'
import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms'
import { zodClient } from 'sveltekit-superforms/adapters'
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { type AddRoleSchema, addRoleSchema } from '$lib/validations/account';
import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
export let availableRoles: { name: string; cuid: string }[] = []
const data: SuperValidated<Infer<AddRoleSchema>> = availableRoles
export let availableRoles: { name: string; cuid: string }[] = [];
const data: SuperValidated<Infer<AddRoleSchema>> = availableRoles;
const form = superForm(data, {
validators: zodClient(addRoleSchema),
@ -18,16 +16,16 @@ const form = superForm(data, {
// toast.error("Please fix the errors in the form.");
// }
// }
})
});
const { form: formData, enhance } = form
const { form: formData, enhance } = form;
function addRole(id: string) {
$formData.roles = [...$formData.roles, id]
$formData.roles = [...$formData.roles, id];
}
function removeRole(id: string) {
$formData.roles = $formData.roles.filter((i) => i !== id)
$formData.roles = $formData.roles.filter((i) => i !== id);
}
</script>

View file

@ -1,10 +1,9 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Button } from '$lib/components/ui/button';
import { User } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import { User } from 'lucide-svelte';
import Ellipsis from 'lucide-svelte/icons/ellipsis';
export let cuid: string;
export let cuid: string;
</script>
<DropdownMenu.Root>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox';
import type { Writable } from 'svelte/store';
import { Checkbox } from '$lib/components/ui/checkbox';
import type { Writable } from 'svelte/store';
export let checked: Writable<boolean>;
export let checked: Writable<boolean>;
</script>
<Checkbox bind:checked={$checked} />

View file

@ -1,135 +1,114 @@
<script lang="ts">
import {
createTable,
Render,
Subscribe,
createRender,
} from "svelte-headless-table";
import {
addPagination,
addSortBy,
addTableFilter,
addHiddenColumns,
addSelectedRows,
} from "svelte-headless-table/plugins";
import { readable } from "svelte/store";
import ArrowUpDown from "lucide-svelte/icons/arrow-up-down";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import * as Table from "$lib/components/ui/table";
import DataTableActions from "./user-table-actions.svelte";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import DataTableCheckbox from "./user-table-checkbox.svelte";
import type { Users } from '$db/schema';
import type { Users } from '$db/schema';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import ArrowUpDown from 'lucide-svelte/icons/arrow-up-down';
import ChevronDown from 'lucide-svelte/icons/chevron-down';
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table';
import { addHiddenColumns, addPagination, addSelectedRows, addSortBy, addTableFilter } from 'svelte-headless-table/plugins';
import { readable } from 'svelte/store';
import DataTableActions from './user-table-actions.svelte';
import DataTableCheckbox from './user-table-checkbox.svelte';
export let users: Users[] = [];
export let users: Users[] = [];
const table = createTable(readable(users), {
page: addPagination(),
sort: addSortBy({ disableMultiSort: true }),
filter: addTableFilter({
fn: ({ filterValue, value }) => value.includes(filterValue),
}),
hide: addHiddenColumns(),
select: addSelectedRows(),
});
const table = createTable(readable(users), {
page: addPagination(),
sort: addSortBy({ disableMultiSort: true }),
filter: addTableFilter({
fn: ({ filterValue, value }) => value.includes(filterValue),
}),
hide: addHiddenColumns(),
select: addSelectedRows(),
});
const columns = table.createColumns([
table.column({
accessor: "cuid",
header: (_, { pluginStates }) => {
const { allPageRowsSelected } = pluginStates.select;
return createRender(DataTableCheckbox, {
checked: allPageRowsSelected,
});
},
cell: ({ row }, { pluginStates }) => {
const { getRowState } = pluginStates.select;
const { isSelected } = getRowState(row);
const columns = table.createColumns([
table.column({
accessor: 'cuid',
header: (_, { pluginStates }) => {
const { allPageRowsSelected } = pluginStates.select;
return createRender(DataTableCheckbox, {
checked: allPageRowsSelected,
});
},
cell: ({ row }, { pluginStates }) => {
const { getRowState } = pluginStates.select;
const { isSelected } = getRowState(row);
return createRender(DataTableCheckbox, {
checked: isSelected,
});
},
plugins: {
filter: {
exclude: true,
},
},
}),
table.column({
accessor: "username",
header: "Username",
}),
table.column({
accessor: "email",
header: "Email",
cell: ({ value }) => {
return value ?? "N/A";
}
}),
table.column({
accessor: "first_name",
header: "First Name",
cell: ({ value }) => {
return value && value.length > 0 ? value : "N/A";
return createRender(DataTableCheckbox, {
checked: isSelected,
});
},
plugins: {
filter: {
exclude: true,
},
plugins: {
filter: {
exclude: true,
},
},
}),
table.column({
accessor: "last_name",
header: "Last Name",
cell: ({ value }) => {
return value && value.length > 0 ? value : "N/A";
},
}),
table.column({
accessor: 'username',
header: 'Username',
}),
table.column({
accessor: 'email',
header: 'Email',
cell: ({ value }) => {
return value ?? 'N/A';
},
}),
table.column({
accessor: 'first_name',
header: 'First Name',
cell: ({ value }) => {
return value && value.length > 0 ? value : 'N/A';
},
plugins: {
filter: {
exclude: true,
},
plugins: {
filter: {
exclude: true,
},
},
}),
table.column({
accessor: 'last_name',
header: 'Last Name',
cell: ({ value }) => {
return value && value.length > 0 ? value : 'N/A';
},
plugins: {
filter: {
exclude: true,
},
}),
table.column({
accessor: ({ cuid }) => cuid,
header: "",
cell: ({ value }) => {
return createRender(DataTableActions, { cuid: value });
},
plugins: {
sort: {
disable: true,
},
},
}),
]);
},
}),
table.column({
accessor: ({ cuid }) => cuid,
header: '',
cell: ({ value }) => {
return createRender(DataTableActions, { cuid: value });
},
plugins: {
sort: {
disable: true,
},
},
}),
]);
const {
headerRows,
pageRows,
tableAttrs,
tableBodyAttrs,
pluginStates,
flatColumns,
rows,
} = table.createViewModel(columns);
const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates, flatColumns, rows } = table.createViewModel(columns);
const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
const { filterValue } = pluginStates.filter;
const { hiddenColumnIds } = pluginStates.hide;
const { selectedDataIds } = pluginStates.select;
const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
const { filterValue } = pluginStates.filter;
const { hiddenColumnIds } = pluginStates.hide;
const { selectedDataIds } = pluginStates.select;
const ids = flatColumns.map((col) => col.id);
let hideForId = Object.fromEntries(ids.map((id) => [id, true]));
const ids = flatColumns.map((col) => col.id);
let hideForId = Object.fromEntries(ids.map((id) => [id, true]));
$: $hiddenColumnIds = Object.entries(hideForId)
.filter(([, hide]) => !hide)
.map(([id]) => id);
$: $hiddenColumnIds = Object.entries(hideForId)
.filter(([, hide]) => !hide)
.map(([id]) => id);
const columnsToHide: string[] = [];
const columnsToHide: string[] = [];
</script>
<div>

View file

@ -1,8 +1,7 @@
<script lang="ts">
import * as Card from '$components/ui/card'
const { data } = $props()
let collections = data?.collections || []
import * as Card from '$components/ui/card';
const { data } = $props();
let collections = data?.collections || [];
</script>
<svelte:head>

View file

@ -1,10 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/stores';
</script>
<div class="error">
{#if $page.status === 404}
<h1>The page you requested doesn't exist! 🤷‍♂️</h1>
<h1>The page you requested doesn't exist! 🤷</h1>
<h3 class="mt-6"><a href="/">Go Home</a></h3>
{:else}
<h1 class="h1">Unexpected Error</h1>

View file

@ -1,13 +1,13 @@
<script lang="ts">
// import { tick, onDestroy } from 'svelte';
import Game from '$components/Game.svelte'
import type { UICollection } from '$lib/types'
import Game from '$components/Game.svelte';
import type { UICollection } from '$lib/types';
const { data } = $props()
const { items = [] } = data
console.log(`Page data: ${JSON.stringify(data)}`)
let collection: UICollection = data?.collection ?? {}
console.log('items', items)
const { data } = $props();
const { items = [] } = data;
console.log(`Page data: ${JSON.stringify(data)}`);
let collection: UICollection = data?.collection ?? {};
console.log('items', items);
// async function handleNextPageEvent(event: CustomEvent) {
// if (+event?.detail?.page === page + 1) {

View file

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

View file

@ -1,7 +1,7 @@
<script lang="ts">
import GameSearchForm from "$components/search/GameSearchForm.svelte";
import GameSearchForm from '$components/search/GameSearchForm.svelte';
export let data;
export let data;
</script>
<h1>Add a game to your collection</h1>

View file

@ -1,20 +1,19 @@
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'
import { notSignedInMessage } from '$lib/flashMessages';
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 { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const form = await superValidate({}, zod(BggForm))
const form = await superValidate({}, zod(BggForm));
return { form }
}
return { form };
};

View file

@ -1,8 +1,8 @@
<script lang="ts">
import Input from "$components/ui/input/input.svelte";
import Label from "$components/ui/label/label.svelte";
import Input from '$components/ui/input/input.svelte';
import Label from '$components/ui/label/label.svelte';
export let data;
export let data;
</script>
<h1>Add a game to your collection</h1>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import Transition from '$components/transition.svelte'
import Transition from '$components/transition.svelte';
export let data
const wishlistsTable = data.wishlists || []
export let data;
const wishlistsTable = data.wishlists || [];
</script>
<aside class="wishlists">

View file

@ -1,6 +1,5 @@
import { notSignedInMessage } from '$lib/flashMessages';
import { db } from '$lib/server/api/packages/drizzle';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { modifyListGameSchema } from '$lib/validations/zod-schemas';
import { type Actions, fail } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';

View file

@ -1,10 +1,10 @@
<script lang="ts">
import Game from '$components/Game.svelte'
import Game from '$components/Game.svelte';
export let data
console.log('data', data)
const wishlist = data.wishlist
const gamesItems = wishlist?.items
export let data;
console.log('data', data);
const wishlist = data.wishlist;
const gamesItems = wishlist?.items;
</script>
<svelte:head>

View file

@ -1,13 +1,13 @@
<script lang="ts">
import { page } from '$app/stores'
import type { Route } from '$lib/types'
import { page } from '$app/stores';
import type { Route } from '$lib/types';
const routes: Route[] = [
{ href: '/settings/profile', label: 'Profile' },
{ href: '/settings/security', label: 'Security' },
]
];
let { children } = $props()
let { children } = $props();
</script>
<div class="security-nav">

View file

@ -1,8 +1,8 @@
// +page.server.ts
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
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')
}
throw redirect(307, '/settings/profile');
};

View file

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

View file

@ -1,10 +1,10 @@
<script>
import { Button } from '$components/ui/button/index'
import { KeyRound } from 'lucide-svelte'
import { Button } from '$components/ui/button/index';
import { KeyRound } from 'lucide-svelte';
const { data } = $props()
const { data } = $props();
const hasSetupTwoFactor = data.hasSetupTwoFactor
const hasSetupTwoFactor = data.hasSetupTwoFactor;
</script>
<div class="mt-6">

View file

@ -1,81 +1,81 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { type Actions, 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'
import { changeUserPasswordSchema } from './schemas'
import { notSignedInMessage } from '$lib/flashMessages';
import { type Actions, 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';
import { changeUserPasswordSchema } from './schemas';
export const load: PageServerLoad = async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const form = await superValidate(event, zod(changeUserPasswordSchema))
const form = await superValidate(event, zod(changeUserPasswordSchema));
form.data = {
current_password: '',
password: '',
confirm_password: '',
}
};
return {
form,
}
}
};
};
export const actions: Actions = {
default: async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const form = await superValidate(event, zod(changeUserPasswordSchema))
const form = await superValidate(event, zod(changeUserPasswordSchema));
if (!form.valid) {
return fail(400, {
form,
})
});
}
const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({
json: { password: form.data.current_password },
})
.then(locals.parseApiResponse)
.then(locals.parseApiResponse);
console.log('verifyPasswordError', verifyPasswordError)
console.log('verifyPasswordError', verifyPasswordError);
if (verifyPasswordError) {
console.error(verifyPasswordError)
return setError(form, 'current_password', 'Your password is incorrect')
console.error(verifyPasswordError);
return setError(form, 'current_password', 'Your password is incorrect');
}
if (authedUser?.username) {
try {
if (form.data.password !== form.data.confirm_password) {
return setError(form, 'Password and confirm password do not match')
return setError(form, 'Password and confirm password do not match');
}
await locals.api.me.update.password.$put({
json: { password: form.data.password, confirm_password: form.data.confirm_password },
})
});
} catch (e) {
console.error(e)
form.data.password = ''
form.data.confirm_password = ''
form.data.current_password = ''
return setError(form, 'current_password', 'Your password is incorrect.')
console.error(e);
form.data.password = '';
form.data.confirm_password = '';
form.data.current_password = '';
return setError(form, 'current_password', 'Your password is incorrect.');
}
const message = {
type: 'success',
message: 'Password Updated. Please sign in.',
} as const
redirect(302, '/login', message, event)
} as const;
redirect(302, '/login', message, event);
}
return setError(form, 'Error occurred. Please try again or contact support if you need further help.')
return setError(form, 'Error occurred. Please try again or contact support if you need further help.');
},
}
};

View file

@ -1,32 +1,32 @@
<script lang="ts">
import * as Alert from '$components/ui/alert'
import * as Form from '$components/ui/form'
import { Input } from '$components/ui/input'
import { Toggle } from '$components/ui/toggle'
import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte'
import { zodClient } from 'sveltekit-superforms/adapters'
import { superForm } from 'sveltekit-superforms/client'
import { changeUserPasswordSchema } from './schemas'
import * as Alert from '$components/ui/alert';
import * as Form from '$components/ui/form';
import { Input } from '$components/ui/input';
import { Toggle } from '$components/ui/toggle';
import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import { changeUserPasswordSchema } from './schemas';
const { data } = $props()
const { data } = $props();
const form = superForm(data.form, {
taintedMessage: null,
validators: zodClient(changeUserPasswordSchema),
delayMs: 500,
multipleSubmits: 'prevent',
})
});
let hiddenCurrentPassword = $state(true)
let hiddenPassword = $state(true)
let hiddenConfirmPassword = $state(true)
let currentPasswordInput = $derived(hiddenCurrentPassword ? 'password' : 'text')
let passwordInput = $derived(hiddenPassword ? 'password' : 'text')
let confirmPasswordInput = $derived(hiddenConfirmPassword ? 'password' : 'text')
let hiddenCurrentPassword = $state(true);
let hiddenPassword = $state(true);
let hiddenConfirmPassword = $state(true);
let currentPasswordInput = $derived(hiddenCurrentPassword ? 'password' : 'text');
let passwordInput = $derived(hiddenPassword ? 'password' : 'text');
let confirmPasswordInput = $derived(hiddenConfirmPassword ? 'password' : 'text');
// $inspect(hiddenCurrentPassword, hiddenPassword, hiddenConfirmPassword)
const { form: formData, enhance } = form
const { form: formData, enhance } = form;
</script>
<form method="POST" use:enhance>

View file

@ -1,4 +1,4 @@
import { z } from 'zod'
import { z } from 'zod';
export const changeUserPasswordSchema = z
.object({
@ -7,15 +7,15 @@ export const changeUserPasswordSchema = z
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx)
})
refinePasswords(confirm_password, password, ctx);
});
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
await comparePasswords(confirm_password, password, ctx)
await checkPasswordStrength(password, ctx)
}
await comparePasswords(confirm_password, password, ctx);
await checkPasswordStrength(password, ctx);
};
const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
if (confirm_password !== password) {
@ -23,52 +23,52 @@ const comparePasswords = async (confirm_password: string, password: string, ctx:
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
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)
const char = password.charAt(i);
if (!Number.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) {
@ -76,6 +76,6 @@ const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) =>
code: 'custom',
message: errorMessage,
path: ['password'],
})
});
}
}
};

View file

@ -1,24 +1,24 @@
import { notSignedInMessage } from '$lib/flashMessages'
import type { Actions } from '@sveltejs/kit'
import { redirect } from 'sveltekit-flash-message/server'
import type { PageServerLoad } from '../../$types'
import { notSignedInMessage } from '$lib/flashMessages';
import type { Actions } from '@sveltejs/kit';
import { redirect } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../$types';
export const load: PageServerLoad = async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
const totpEnabled = !!totpData
const totpEnabled = !!totpData;
return {
totpEnabled,
hardwareTokenEnabled: false,
}
}
};
};
export const actions: Actions = {}
export const actions: Actions = {};

View file

@ -1,12 +1,12 @@
<script lang="ts">
import { Badge } from '$components/ui/badge'
import { Button } from '$components/ui/button'
import * as Card from '$lib/components/ui/card'
import { Badge } from '$components/ui/badge';
import { Button } from '$components/ui/button';
import * as Card from '$components/ui/card';
const { data } = $props()
const { data } = $props();
const totpEnabled = data.totpEnabled
const hardwareTokenEnabled = data.hardwareTokenEnabled
const totpEnabled = data.totpEnabled;
const hardwareTokenEnabled = data.hardwareTokenEnabled;
</script>
<h1>Two-factor authentication</h1>

View file

@ -1,28 +1,28 @@
import { notSignedInMessage } from '$lib/flashMessages'
import { redirect } from 'sveltekit-flash-message/server'
import type { PageServerLoad } from '../../../$types'
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 { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
if (authedUser.mfa_enabled) {
const { data: recoveryCodesData, error: recoveryCodesError } = await locals.api.mfa.totp.recoveryCodes.$get().then(locals.parseApiResponse)
console.log('recoveryCodesData', recoveryCodesData)
console.log('recoveryCodesError', recoveryCodesError)
const { data: recoveryCodesData, error: recoveryCodesError } = await locals.api.mfa.totp.recoveryCodes.$get().then(locals.parseApiResponse);
console.log('recoveryCodesData', recoveryCodesData);
console.log('recoveryCodesError', recoveryCodesError);
if (recoveryCodesError || !recoveryCodesData || !recoveryCodesData.recoveryCodes) {
return {
recoveryCodes: [],
}
};
}
return {
recoveryCodes: recoveryCodesData.recoveryCodes,
}
};
}
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event)
}
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event);
};

View file

@ -0,0 +1,5 @@
<script lang="ts"></script>
<h1>Security Keys</h1>
<p>TODO</p>

View file

@ -1,35 +1,35 @@
import { notSignedInMessage } from '$lib/flashMessages'
import env from '$lib/server/api/common/env'
import { decodeHex, encodeBase32 } from '@oslojs/encoding'
import { createTOTPKeyURI } from '@oslojs/otp'
import { type Actions, fail } from '@sveltejs/kit'
import kebabCase from 'just-kebab-case'
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'
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas'
import { notSignedInMessage } from '$lib/flashMessages';
import env from '$lib/server/api/common/env';
import { decodeHex, encodeBase32 } from '@oslojs/encoding';
import { createTOTPKeyURI } from '@oslojs/otp';
import { type Actions, fail } from '@sveltejs/kit';
import kebabCase from 'just-kebab-case';
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';
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas';
export const load: PageServerLoad = async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema))
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema))
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
// const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (error || !data) {
return fail(500, {
addTwoFactorForm,
})
});
}
const { totpCredential } = data
const { totpCredential } = data;
if (totpCredential && authedUser.mfa_enabled) {
return {
addTwoFactorForm,
@ -38,41 +38,41 @@ export const load: PageServerLoad = async (event) => {
recoveryCodes: [],
totpUri: '',
qrCode: '',
}
};
}
if (totpCredential && !authedUser.mfa_enabled) {
await locals.api.mfa.totp.$delete().then(locals.parseApiResponse)
await locals.api.mfa.totp.$delete().then(locals.parseApiResponse);
}
const issuer = kebabCase(env.PUBLIC_SITE_NAME)
const accountName = authedUser.email || authedUser.username
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse)
const issuer = kebabCase(env.PUBLIC_SITE_NAME);
const accountName = authedUser.email || authedUser.username;
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse);
if (createdTotpError || !createdTotpData) {
return fail(500, {
addTwoFactorForm,
})
});
}
const { totpCredential: createdTotpCredentials } = createdTotpData
const { totpCredential: createdTotpCredentials } = createdTotpData;
// pass the website's name and the user identifier (e.g. email, username)
if (!createdTotpCredentials?.secret_data) {
return fail(500, {
addTwoFactorForm,
})
});
}
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data)
const secret = encodeBase32(decodedHexSecret)
const intervalInSeconds = 30
const digits = 6
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data);
const secret = encodeBase32(decodedHexSecret);
const intervalInSeconds = 30;
const digits = 6;
const totpUri = createTOTPKeyURI(issuer, accountName, decodedHexSecret, intervalInSeconds, digits)
const totpUri = createTOTPKeyURI(issuer, accountName, decodedHexSecret, intervalInSeconds, digits);
addTwoFactorForm.data = {
password: '',
two_factor_code: '',
}
};
return {
addTwoFactorForm,
removeTwoFactorForm,
@ -81,84 +81,84 @@ export const load: PageServerLoad = async (event) => {
totpUri,
qrCode: await QRCode.toDataURL(totpUri),
secret,
}
}
};
};
export const actions: Actions = {
enableTotp: async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema))
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
if (!addTwoFactorForm.valid) {
return fail(400, {
addTwoFactorForm,
})
});
}
const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({
json: { password: addTwoFactorForm.data.password },
})
.then(locals.parseApiResponse)
.then(locals.parseApiResponse);
if (verifyPasswordError) {
console.log(verifyPasswordError)
return setError(addTwoFactorForm, 'password', 'Your password is incorrect')
console.log(verifyPasswordError);
return setError(addTwoFactorForm, 'password', 'Your password is incorrect');
}
if (addTwoFactorForm.data.two_factor_code === '') {
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code')
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code');
}
const twoFactorCode = addTwoFactorForm.data.two_factor_code
const twoFactorCode = addTwoFactorForm.data.two_factor_code;
const { error: verifyTotpError } = await locals.api.mfa.totp.verify
.$post({
json: { code: twoFactorCode },
})
.then(locals.parseApiResponse)
.then(locals.parseApiResponse);
if (verifyTotpError) {
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code')
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code');
}
redirect(302, '/settings/security/mfa/recovery-codes')
redirect(302, '/settings/security/mfa/recovery-codes');
},
disableTotp: async (event) => {
const { locals } = event
const { locals } = event;
const authedUser = await locals.getAuthedUser()
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
throw redirect(302, '/login', notSignedInMessage, event);
}
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema))
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
if (!removeTwoFactorForm.valid) {
return fail(400, {
removeTwoFactorForm,
})
});
}
const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({
json: { password: removeTwoFactorForm.data.password },
})
.then(locals.parseApiResponse)
.then(locals.parseApiResponse);
if (verifyPasswordError) {
console.log(verifyPasswordError)
return setError(removeTwoFactorForm, 'password', 'Your password is incorrect')
console.log(verifyPasswordError);
return setError(removeTwoFactorForm, 'password', 'Your password is incorrect');
}
const { error: deleteTotpError } = await locals.api.mfa.totp.$delete().then(locals.parseApiResponse)
const { error: deleteTotpError } = await locals.api.mfa.totp.$delete().then(locals.parseApiResponse);
if (deleteTotpError) {
return fail(500, {
removeTwoFactorForm,
})
});
}
redirect(
@ -169,6 +169,6 @@ export const actions: Actions = {
message: 'Two-Factor Authentication has been disabled.',
},
event,
)
);
},
}
};

View file

@ -1,36 +1,34 @@
<script lang="ts">
import CopyCodeBlock from '$components/CopyCodeBlock.svelte'
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'
import CopyCodeBlock from '$components/CopyCodeBlock.svelte';
import PinInput from '$components/pin-input.svelte';
import * as Form from '$components/ui/form';
import { Input } from '$components/ui/input';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas';
const { data } = $props()
const { data } = $props();
const { qrCode, secret, twoFactorEnabled, recoveryCodes } = 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)
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes);
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm;
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm;
</script>
<section class="two-factor">

View file

@ -1,14 +1,14 @@
import { z } from 'zod'
import { z } from 'zod';
export const addTwoFactorSchema = z.object({
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 type AddTwoFactorSchema = typeof addTwoFactorSchema;
export const removeTwoFactorSchema = addTwoFactorSchema.pick({
password: true,
})
});
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema;

View file

@ -1,8 +1,7 @@
<script lang="ts">
import * as Card from '$components/ui/card'
const { data } = $props()
const { wishlists = [] } = data
import * as Card from '$components/ui/card';
const { data } = $props();
const { wishlists = [] } = data;
</script>
<svelte:head>

View file

@ -1,8 +1,7 @@
import { notSignedInMessage } from '$lib/flashMessages.js';
import { db } from '$lib/server/api/packages/drizzle';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { modifyListGameSchema } from '$lib/validations/zod-schemas';
import { type Actions, error, fail } from '@sveltejs/kit';
import { type Actions, error } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
import { redirect } from 'sveltekit-flash-message/server';
import { zod } from 'sveltekit-superforms/adapters';

View file

@ -1,8 +1,8 @@
<script lang="ts">
import Game from '$components/Game.svelte'
import Game from '$components/Game.svelte';
const { data } = $props()
const { items = [] } = data
const { data } = $props();
const { items = [] } = data;
</script>
<svelte:head>

View file

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

View file

@ -1,9 +1,9 @@
<script lang="ts">
import 'iconify-icon'
import Footer from '$components/Footer.svelte'
import Header from '$components/Header.svelte'
import 'iconify-icon';
import Footer from '$components/Footer.svelte';
import Header from '$components/Header.svelte';
const { data, children } = $props()
const { data, children } = $props();
</script>
<div class="flex min-h-screen w-full flex-col">

View file

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

View file

@ -1,15 +1,14 @@
<script lang="ts">
import AddToList from '$components/AddToList.svelte'
import Badge from '$components/ui/badge/badge.svelte'
import { Button } from '$components/ui/button'
import { Dices, ExternalLinkIcon, MinusIcon, PlusIcon } from 'lucide-svelte'
import { Image } from 'svelte-lazy-loader'
import type { PageData } from './$types'
import AddToList from '$components/AddToList.svelte';
import Badge from '$components/ui/badge/badge.svelte';
import { Button } from '$components/ui/button';
import { Dices, ExternalLinkIcon, MinusIcon, PlusIcon } from 'lucide-svelte';
import { Image } from 'svelte-lazy-loader';
const { data } = $props()
const { game, user, in_collection, in_wishlist } = data
const { data } = $props();
const { game, user, in_collection, in_wishlist } = data;
let seeMore: boolean = $state(false)
let seeMore: boolean = $state(false);
</script>
<svelte:head>

View file

@ -1,65 +1,65 @@
import type { GameType, SearchQuery } from '$lib/types'
import { createOrUpdateGameMinimal } from '$lib/utils/db/gameUtils'
import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js'
import { search_schema } from '$lib/zodValidation'
import { error } from '@sveltejs/kit'
import type { BggThingDto } from 'boardgamegeekclient/dist/esm/dto/index.js'
import kebabCase from 'just-kebab-case'
import { zod } from 'sveltekit-superforms/adapters'
import { superValidate } from 'sveltekit-superforms/server'
import type { GameType, SearchQuery } from '$lib/types';
import { createOrUpdateGameMinimal } from '$lib/utils/db/gameUtils';
import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js';
import { search_schema } from '$lib/zodValidation';
import { error } from '@sveltejs/kit';
import type { BggThingDto } from 'boardgamegeekclient/dist/esm/dto/index.js';
import kebabCase from 'just-kebab-case';
import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server';
async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQueryParams: URLSearchParams) {
try {
console.log('urlQueryParams search games', urlQueryParams)
console.log('urlQueryParams search games', urlQueryParams);
const headers = new Headers()
headers.set('Content-Type', 'application/json')
const headers = new Headers();
headers.set('Content-Type', 'application/json');
const requestInit: RequestInit = {
method: 'GET',
headers,
}
const url = `/api/games/search${urlQueryParams ? `?${urlQueryParams}` : ''}`
console.log('Calling internal api', url)
const response = await eventFetch(url, requestInit)
console.log('response from internal api', response)
};
const url = `/api/games/search${urlQueryParams ? `?${urlQueryParams}` : ''}`;
console.log('Calling internal api', url);
const response = await eventFetch(url, requestInit);
console.log('response from internal api', response);
if (response.status !== 404 && !response.ok) {
console.log('Status from internal api not 200', response.status)
error(response.status)
console.log('Status from internal api not 200', response.status);
error(response.status);
}
const games = await response.json()
console.log('games from DB', games)
const games = await response.json();
console.log('games from DB', games);
const gameNameSearch = urlQueryParams.get('q') ?? ''
let totalCount = games?.length || 0
const gameNameSearch = urlQueryParams.get('q') ?? '';
let totalCount = games?.length || 0;
if (totalCount === 0 || !games.find((game: GameType) => game.slug === kebabCase(gameNameSearch))) {
console.log('No games found in DB for', gameNameSearch)
const searchQueryParams = urlQueryParams ? `?${urlQueryParams}` : ''
const externalResponse = await eventFetch(`/api/external/search${searchQueryParams}`, requestInit)
console.log('No games found in DB for', gameNameSearch);
const searchQueryParams = urlQueryParams ? `?${urlQueryParams}` : '';
const externalResponse = await eventFetch(`/api/external/search${searchQueryParams}`, requestInit);
console.log('Back from external search', externalResponse)
console.log('Back from external search', externalResponse);
if (!externalResponse.ok) {
console.log('Status not 200', externalResponse.status)
error(externalResponse.status)
console.log('Status not 200', externalResponse.status);
error(externalResponse.status);
}
if (externalResponse.ok) {
const gameResponse = await externalResponse.json()
console.log('response from external api', gameResponse)
const gameList: BggThingDto[] = gameResponse?.games
totalCount = gameResponse?.totalCount
console.log('totalCount', totalCount)
const gameResponse = await externalResponse.json();
console.log('response from external api', gameResponse);
const gameList: BggThingDto[] = gameResponse?.games;
totalCount = gameResponse?.totalCount;
console.log('totalCount', totalCount);
for (const game of gameList) {
console.log(`Retrieving simplified external game details for id: ${game.id} with name ${game.name}`)
const externalGameResponse = await eventFetch(`/api/external/game/${game.id}?simplified=true`)
console.log(`Retrieving simplified external game details for id: ${game.id} with name ${game.name}`);
const externalGameResponse = await eventFetch(`/api/external/game/${game.id}?simplified=true`);
if (externalGameResponse.ok) {
const externalGame = await externalGameResponse.json()
console.log('externalGame', externalGame)
const boredGame = mapAPIGameToBoredGame(externalGame)
games.push(createOrUpdateGameMinimal(locals, boredGame, externalGame.id))
const externalGame = await externalGameResponse.json();
console.log('externalGame', externalGame);
const boredGame = mapAPIGameToBoredGame(externalGame);
games.push(createOrUpdateGameMinimal(locals, boredGame, externalGame.id));
}
}
}
@ -68,14 +68,14 @@ async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQ
return {
totalCount,
games,
}
};
} catch (e) {
console.log(`Error searching board games ${e}`)
console.log(`Error searching board games ${e}`);
}
return {
totalCount: 0,
games: [],
}
};
}
const defaults = {
@ -85,14 +85,14 @@ const defaults = {
sort: 'asc',
q: '',
exact: false,
}
};
export const load = async ({ locals, fetch, url }) => {
const searchParams = Object.fromEntries(url?.searchParams)
console.log('searchParams', searchParams)
searchParams.order = searchParams.order || defaults.order
searchParams.sort = searchParams.sort || defaults.sort
searchParams.q = searchParams.q || defaults.q
const searchParams = Object.fromEntries(url?.searchParams);
console.log('searchParams', searchParams);
searchParams.order = searchParams.order || defaults.order;
searchParams.sort = searchParams.sort || defaults.sort;
searchParams.q = searchParams.q || defaults.q;
const form = await superValidate(
{
...searchParams,
@ -101,14 +101,14 @@ export const load = async ({ locals, fetch, url }) => {
exact: searchParams.exact ? searchParams.exact === 'true' : defaults.exact,
},
zod(search_schema),
)
);
const queryParams: SearchQuery = {
limit: form.data?.limit,
skip: form.data?.skip,
q: form.data?.q,
exact: form.data?.exact,
}
};
try {
if (form.data?.q === '') {
@ -119,53 +119,53 @@ export const load = async ({ locals, fetch, url }) => {
games: [],
wishlists: [],
},
}
};
}
if (form.data?.minAge) {
if (form.data?.exactMinAge) {
queryParams.min_age = form.data?.minAge
queryParams.min_age = form.data?.minAge;
} else {
queryParams.gt_min_age = form.data?.minAge === 1 ? 0 : form.data?.minAge - 1
queryParams.gt_min_age = form.data?.minAge === 1 ? 0 : form.data?.minAge - 1;
}
}
if (form.data?.minPlayers) {
if (form.data?.exactMinPlayers) {
queryParams.min_players = form.data?.minPlayers
queryParams.min_players = form.data?.minPlayers;
} else {
queryParams.gt_min_players = form.data?.minPlayers === 1 ? 0 : form.data?.minPlayers - 1
queryParams.gt_min_players = form.data?.minPlayers === 1 ? 0 : form.data?.minPlayers - 1;
}
}
if (form.data?.maxPlayers) {
if (form.data?.exactMaxPlayers) {
queryParams.max_players = form.data?.maxPlayers
queryParams.max_players = form.data?.maxPlayers;
} else {
queryParams.lt_max_players = form.data?.maxPlayers + 1
queryParams.lt_max_players = form.data?.maxPlayers + 1;
}
}
const newQueryParams: Record<string, string> = {}
const newQueryParams: Record<string, string> = {};
for (const key in queryParams) {
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
}
const urlQueryParams = new URLSearchParams(newQueryParams)
const searchData = await searchForGames(locals, fetch, urlQueryParams)
const urlQueryParams = new URLSearchParams(newQueryParams);
const searchData = await searchForGames(locals, fetch, urlQueryParams);
console.log('search data', JSON.stringify(searchData, null, 2))
console.log('search data', JSON.stringify(searchData, null, 2));
return {
form,
// modifyListForm,
searchData,
wishlists: [],
}
};
} catch (e) {
console.log(`Error searching board games ${e}`)
console.log(`Error searching board games ${e}`);
}
console.log('returning default no data')
console.log('returning default no data');
return {
form,
searchData: {
@ -173,29 +173,29 @@ export const load = async ({ locals, fetch, url }) => {
games: [],
},
wishlists: [],
}
}
};
};
export const actions = {
random: async ({ request, locals, fetch }) => {
const form = await superValidate(request, zod(search_schema))
const form = await superValidate(request, zod(search_schema));
const queryParams: SearchQuery = {
order_by: 'rank',
ascending: false,
random: true,
fields: 'id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description',
}
};
const newQueryParams: Record<string, string> = {}
const newQueryParams: Record<string, string> = {};
for (const key in queryParams) {
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
}
const urlQueryParams = new URLSearchParams(newQueryParams)
const urlQueryParams = new URLSearchParams(newQueryParams);
return {
form,
searchData: await searchForGames(locals, fetch, urlQueryParams),
}
};
},
}
};

Some files were not shown because too many files have changed in this diff Show more