Migrate to Svelte 5 and upgrade dependencies

This commit is contained in:
Evert Prants 2024-11-23 18:03:31 +02:00
parent 4ab7145ca5
commit 13fc448487
Signed by: evert
GPG Key ID: 0960A17F9F40237D
51 changed files with 1757 additions and 1513 deletions

2502
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,46 +12,47 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/kit": "^2.5.15",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@sveltejs/kit": "^2.8.2",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@types/bcryptjs": "^2.4.6",
"@types/eslint": "^8.56.10",
"@types/eslint": "^9.6.1",
"@types/mime-types": "^2.1.4",
"@types/node": "^20.14.2",
"@types/nodemailer": "^6.4.15",
"@types/node": "^22.9.3",
"@types/nodemailer": "^6.4.17",
"@types/qrcode": "^1.5.5",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"drizzle-kit": "^0.22.7",
"eslint": "^8.56.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"drizzle-kit": "^0.28.1",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.39.3",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.4",
"svelte": "^4.2.18",
"svelte-check": "^3.8.0",
"tslib": "^2.6.3",
"typescript": "^5.4.5",
"vite": "^5.3.1"
"eslint-plugin-svelte": "^2.46.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.2.7",
"svelte-check": "^4.1.0",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"vite": "^5.4.11"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-node": "^5.1.0",
"@sveltejs/adapter-node": "^5.2.9",
"bcryptjs": "^2.4.3",
"cropperjs": "^1.6.2",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.31.2",
"drizzle-orm": "^0.36.4",
"image-size": "^1.1.1",
"jose": "^5.4.0",
"jose": "^5.9.6",
"mime-types": "^2.1.35",
"mysql2": "^3.10.1",
"nodemailer": "^6.9.13",
"mysql2": "^3.11.4",
"nodemailer": "^6.9.16",
"otplib": "^12.0.1",
"qrcode": "^1.5.3",
"qrcode": "^1.5.4",
"svelte-kit-cookie-session": "^4.0.0",
"sveltekit-i18n": "^2.4.2",
"sveltekit-rate-limiter": "^0.5.1",
"uuid": "^10.0.0"
"sveltekit-rate-limiter": "^0.6.1",
"uuid": "^11.0.3",
"vite-plugin-mkcert": "^1.17.6"
}
}

View File

@ -4,14 +4,24 @@
import { enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
export let action: string;
export let variant: ComponentProps<Button>['variant'] = 'link';
interface Props {
action: string;
variant?: ComponentProps<typeof Button>['variant'];
enhanced?: boolean;
enhanceFn?: SubmitFunction | undefined;
children?: import('svelte').Snippet;
}
export let enhanced = false;
export let enhanceFn: SubmitFunction | undefined = undefined;
$: enhancer = enhanced ? enhance : () => {};
let {
action,
variant = 'link',
enhanced = false,
enhanceFn = undefined,
children
}: Props = $props();
let enhancer = $derived(enhanced ? enhance : () => {});
</script>
<form {action} method="POST" use:enhancer={enhanceFn}>
<Button {variant} type="submit"><slot /></Button>
<Button {variant} type="submit">{@render children?.()}</Button>
</form>

View File

@ -2,15 +2,20 @@
import { createEventDispatcher } from 'svelte';
import Button from './Button.svelte';
export let type: 'default' | 'error' | 'success' = 'default';
export let dismissable = false;
interface Props {
type?: 'default' | 'error' | 'success';
dismissable?: boolean;
children?: import('svelte').Snippet;
}
let { type = 'default', dismissable = false, children }: Props = $props();
const dispatch = createEventDispatcher();
</script>
<div class="alert alert-{type}" role="alert">
<p><slot /></p>
{#if dismissable}<Button variant="link" on:click={() => dispatch('dismiss')}>x</Button>{/if}
<p>{@render children?.()}</p>
{#if dismissable}<Button variant="link" onclick={() => dispatch('dismiss')}>x</Button>{/if}
</div>
<style>

View File

@ -1,19 +1,25 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface Props {
type?: 'button' | 'submit';
variant?: 'default' | 'primary' | 'link';
formaction?: string | undefined;
disabled?: boolean;
children?: import('svelte').Snippet;
onclick?: (e: MouseEvent) => void;
}
export let type: 'button' | 'submit' = 'button';
export let variant: 'default' | 'primary' | 'link' = 'default';
export let formaction: string | undefined = undefined;
export let disabled = false;
const dispath = createEventDispatcher();
let {
type = 'button',
variant = 'default',
formaction = undefined,
disabled = false,
children,
onclick
}: Props = $props();
</script>
<button
{type}
{formaction}
class="btn btn-{variant}"
on:click={(e) => dispath('click', e)}
{disabled}><slot /></button
<button {type} {formaction} class="btn btn-{variant}" {onclick} {disabled}
>{@render children?.()}</button
>
<style>

View File

@ -1,38 +1,55 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { self } from '$lib/utils';
export let showModal: boolean;
export let dismissable = true;
let dialog: HTMLDialogElement;
interface Props {
showModal: boolean;
dismissable?: boolean;
header?: import('svelte').Snippet;
children?: import('svelte').Snippet;
footer?: import('svelte').Snippet;
onclose?: () => void;
}
const dispatch = createEventDispatcher();
let {
showModal = $bindable(),
dismissable = true,
header,
children,
footer,
onclose
}: Props = $props();
let dialog: HTMLDialogElement = $state()!;
$: if (dialog && showModal) dialog.showModal();
$: if (dialog?.open && !showModal) dialog.close();
$effect(() => {
if (dialog) {
if (showModal) dialog.showModal();
if (dialog.open && !showModal) dialog.close();
}
});
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
<dialog
bind:this={dialog}
on:close={() => {
onclose={() => {
showModal = false;
dispatch('close');
onclose?.();
}}
on:click|self={() => dismissable && dialog.close()}
onclick={self(() => dismissable && dialog.close())}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click|stopPropagation>
{#if $$slots.header}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onclick={(e) => e.stopPropagation()}>
{#if header}
<div class="dialog-header">
<slot name="header" />
{@render header?.()}
</div>
{/if}
<div class="dialog-body">
<slot />
{@render children?.()}
</div>
{#if $$slots.footer}
{#if footer}
<div class="dialog-footer">
<slot name="footer" />
{@render footer?.()}
</div>
{/if}
</div>

View File

@ -3,11 +3,15 @@
import { page } from '$app/stores';
import { t } from '$lib/i18n';
export let meta: PaginationMeta;
$: pageNum = Number($page.url.searchParams.get('page')) || 1;
$: firstPage = pageNum === 1;
$: lastPage = pageNum === meta.pageCount;
$: pageButtons = Array.from({ length: meta.pageCount }, (_, i) => i + 1);
interface Props {
meta: PaginationMeta;
}
let { meta }: Props = $props();
let pageNum = $derived(Number($page.url.searchParams.get('page')) || 1);
let firstPage = $derived(pageNum === 1);
let lastPage = $derived(!meta.pageCount || pageNum === meta.pageCount);
let pageButtons = $derived(Array.from({ length: meta.pageCount }, (_, i) => i + 1));
const makePageUrl = (params: URLSearchParams, pageNumber: number) => {
const searchParams = new URLSearchParams(params);

View File

@ -5,8 +5,8 @@
import ActionButton from './ActionButton.svelte';
import Icon from './icons/Icon.svelte';
$: nextMode = ($themeMode === 'dark' ? 'light' : 'dark') as ThemeModeType;
$: iconName = $themeMode === 'light' ? 'DarkMode' : 'LightMode';
let nextMode = $derived(($themeMode === 'dark' ? 'light' : 'dark') as ThemeModeType);
let iconName = $derived($themeMode === 'light' ? 'DarkMode' : 'LightMode');
const enhanceFn: SubmitFunction = async ({ action, cancel }) => {
const theme = action.searchParams.get('themeMode') as ThemeModeType;

View File

@ -5,15 +5,19 @@
import Icon from '../icons/Icon.svelte';
import AdminDateTime from './AdminDateTime.svelte';
export let audit: PageData['list'][0];
let expanded = false;
interface Props {
audit: PageData['list'][0];
}
let { audit }: Props = $props();
let expanded = $state(false);
</script>
<div class="audit-log{expanded ? ' expanded' : ''}{audit.flagged ? ' flagged' : ''}">
<div class="audit-log-title">
<span class="audit-log-action">{audit.action}</span>
<span class="audit-log-stamp"><AdminDateTime date={audit.created_at} /></span>
<Button on:click={() => (expanded = !expanded)}><Icon icon="ChevronDown" /></Button>
<Button onclick={() => (expanded = !expanded)}><Icon icon="ChevronDown" /></Button>
</div>
{#if expanded}

View File

@ -3,7 +3,11 @@
import type { PageData } from '../../../routes/ssoadmin/oauth2/$types';
import AdminDateTime from './AdminDateTime.svelte';
export let client: PageData['list'][0];
interface Props {
client: PageData['list'][0];
}
let { client }: Props = $props();
</script>
<div class="client">

View File

@ -2,9 +2,13 @@
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
const formatDate = dateFormat.format.bind(null);
export let date: Date;
interface Props {
date: Date;
}
$: dateStr = formatDate(date);
let { date }: Props = $props();
let dateStr = $derived(formatDate(date));
</script>
{dateStr}

View File

@ -2,7 +2,11 @@
import { env } from '$env/dynamic/public';
import type { UserSession } from '$lib/types';
import ThemeButton from '../ThemeButton.svelte';
export let user: UserSession;
interface Props {
user: UserSession;
}
let { user }: Props = $props();
</script>
<header class="admin-header">

View File

@ -10,15 +10,21 @@
name: string;
}
export let available: PrivilegeType[];
export let current: PrivilegeType[];
interface Props {
available: PrivilegeType[];
current: PrivilegeType[];
}
let currentState = [...current];
$: availableState = available.filter(({ id }) => !currentState.some((entry) => entry.id === id));
$: selectedIds = currentState.map(({ id }) => id);
let { available, current }: Props = $props();
let availableSelect: HTMLSelectElement;
let currentSelect: HTMLSelectElement;
let currentState = $state([...current]);
let availableState = $derived(
available.filter(({ id }) => !currentState.some((entry) => entry.id === id))
);
let selectedIds = $derived(currentState.map(({ id }) => id));
let availableSelect: HTMLSelectElement = $state()!;
let currentSelect: HTMLSelectElement = $state()!;
const transferToCurrent = () => {
currentState = [
@ -56,8 +62,8 @@
</ColumnView>
<ColumnView>
<Button variant="primary" on:click={transferToAvailable}>&lt;&lt;</Button>
<Button variant="primary" on:click={transferToCurrent}>&gt;&gt;</Button>
<Button variant="primary" onclick={transferToAvailable}>&lt;&lt;</Button>
<Button variant="primary" onclick={transferToCurrent}>&gt;&gt;</Button>
</ColumnView>
<ColumnView>

View File

@ -4,7 +4,11 @@
import { page } from '$app/stores';
import { t } from '$lib/i18n';
export let user: UserSession;
interface Props {
user: UserSession;
}
let { user }: Props = $props();
const links = [
{
@ -24,7 +28,7 @@
}
];
$: entries = links.filter((link) => hasPrivileges(user.privileges || [], link.privileges));
let entries = $derived(links.filter((link) => hasPrivileges(user.privileges || [], link.privileges)));
</script>
<aside class="admin-sidebar">

View File

@ -3,7 +3,11 @@
import AdminDateTime from './AdminDateTime.svelte';
import type { PageData } from '../../../routes/ssoadmin/users/$types';
export let user: PageData['list'][0];
interface Props {
user: PageData['list'][0];
}
let { user }: Props = $props();
</script>
<div class="user">

View File

@ -1,15 +1,20 @@
<script lang="ts">
export let src = '';
export let alt = '';
interface Props {
src?: string;
alt?: string;
children?: import('svelte').Snippet;
}
let { src = '', alt = '', children }: Props = $props();
</script>
<div class="avatar-wrapper{$$slots.default ? ' with-actions' : ''}">
<div class="avatar-wrapper{children ? ' with-actions' : ''}">
<div class="image-wrapper">
<img {src} {alt} />
</div>
<div class="actions-wrapper">
<slot />
{@render children?.()}
</div>
</div>

View File

@ -9,15 +9,19 @@
import FormControl from '../form/FormControl.svelte';
import { ALLOWED_IMAGES } from '$lib/constants';
export let show: Writable<boolean>;
export let url: string = '/account';
interface Props {
show: Writable<boolean>;
url?: string;
}
let { show, url = '/account' }: Props = $props();
let cropper: Cropper;
let image: HTMLImageElement;
let resultImage: HTMLImageElement;
let image: HTMLImageElement = $state()!;
let resultImage: HTMLImageElement = $state()!;
let resultBlob: Blob | null = null;
let picker = true;
let ready = false;
let picker = $state(true);
let ready = $state(false);
const resetCropper = () => {
cropper?.destroy();
@ -89,21 +93,21 @@
await invalidateAll();
};
$: if (!$show && image?.src) reset();
$effect(() => {
if (!$show && image?.src) reset();
});
</script>
<Modal showModal={$show} on:close={() => ($show = false)}>
<span slot="header">{$t('account.avatar.change')}</span>
<Modal showModal={$show} onclose={() => ($show = false)}>
{#snippet header()}
<span>{$t('account.avatar.change')}</span>
{/snippet}
<div>
{#if picker}
<FormControl>
<label for="avatar-file">{$t('account.avatar.uploadLabel')}</label>
<input
type="file"
on:change={(e) => readFile(e.target)}
accept={ALLOWED_IMAGES.join(',')}
/>
<input type="file" onchange={(e) => readFile(e.target)} accept={ALLOWED_IMAGES.join(',')} />
<span>{$t('account.avatar.hint')}</span>
</FormControl>
{/if}
@ -117,21 +121,23 @@
</div>
</div>
<div class="actions" slot="footer">
<Button variant="link" on:click={() => ($show = false)}>{$t('common.cancel')}</Button>
{#snippet footer()}
<div class="actions">
<Button variant="link" onclick={() => ($show = false)}>{$t('common.cancel')}</Button>
{#if !picker}
<Button variant="primary" on:click={() => reset()}>{$t('account.avatar.restart')}</Button>
{#if !picker}
<Button variant="primary" onclick={() => reset()}>{$t('account.avatar.restart')}</Button>
{#if !ready}
<Button variant="primary" on:click={() => createCropResult()}
>{$t('account.avatar.done')}</Button
>
{:else}
<Button variant="primary" on:click={() => upload()}>{$t('account.avatar.upload')}</Button>
{#if !ready}
<Button variant="primary" onclick={() => createCropResult()}
>{$t('account.avatar.done')}</Button
>
{:else}
<Button variant="primary" onclick={() => upload()}>{$t('account.avatar.upload')}</Button>
{/if}
{/if}
{/if}
</div>
</div>
{/snippet}
</Modal>
<style>

View File

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="button-row">
<slot />
{@render children?.()}
</div>
<style>

View File

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="column">
<slot />
{@render children?.()}
</div>
<style>

View File

@ -1,7 +1,15 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<main>
<div class="page-wrapper">
<div class="page-inner">
<slot />
{@render children?.()}
</div>
</div>
</main>

View File

@ -1,6 +1,14 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<main class="aside-wrapper">
<div class="aside-inner">
<slot />
{@render children?.()}
</div>
</main>

View File

@ -1,6 +1,15 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
side?: import('svelte').Snippet;
}
let { children, side }: Props = $props();
</script>
<div class="split">
<slot />
<slot name="side" />
{@render children?.()}
{@render side?.()}
</div>
<style>

View File

@ -1,6 +1,15 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
action?: import('svelte').Snippet;
}
let { children, action }: Props = $props();
</script>
<div class="title-row">
<slot />
<slot name="action" />
{@render children?.()}
{@render action?.()}
</div>
<style>

View File

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="form-control">
<slot></slot>
{@render children?.()}
</div>
<style>
@ -23,7 +31,7 @@
outline: var(--in-focus-outline);
}
&[disabled] {
&:global([disabled]) {
background-color: var(--in-input-background-disabled);
border-color: var(--in-input-border-color-disabled);
color: var(--in-input-color-disabled);
@ -66,14 +74,14 @@
position: relative;
}
.form-control > :global(label):has(+ input[required])::after {
.form-control > :global(label):has(:global(+ input[required]))::after {
content: '*';
color: var(--in-input-required-color);
margin-left: 4px;
font-weight: 700;
}
.form-control:has(input[type='checkbox']) {
.form-control:global(:has(input[type='checkbox'])) {
display: grid;
grid-template-areas: 'box label' 'hint hint';
grid-template-columns: min-content auto;
@ -82,11 +90,11 @@
align-items: center;
}
.form-control:has(input[type='checkbox']) > :global(span) {
.form-control:global(:has(input[type='checkbox'])) > :global(span) {
grid-area: hint;
}
.form-control:has(input[type='checkbox']) > :global(label) {
.form-control:global(:has(input[type='checkbox'])) > :global(label) {
grid-area: label;
margin: 0;
}

View File

@ -2,8 +2,12 @@
import { t } from '$lib/i18n';
import Alert from '../Alert.svelte';
export let prefix: string;
export let errors: string[] = [];
interface Props {
prefix: string;
errors?: string[];
}
let { prefix, errors = [] }: Props = $props();
</script>
{#if errors.length}

View File

@ -1,14 +1,19 @@
<script lang="ts">
import { t } from '$lib/i18n';
export let title: string = '';
export let required = false;
interface Props {
title?: string;
required?: boolean;
children?: import('svelte').Snippet;
}
let { title = '', required = false, children }: Props = $props();
</script>
<div class="form-section">
{#if title}<div class="form-subtitle">{title}</div>{/if}
{#if required}<span class="form-required">{$t('common.required')}</span>{/if}
<slot />
{@render children?.()}
</div>
<style>

View File

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="form-wrapper">
<slot />
{@render children?.()}
</div>
<style>

View File

@ -1,12 +1,17 @@
<script lang="ts">
export let icon: string;
interface Props {
icon: string;
}
$: iconComponent = import(`./svg/${icon}.svelte`);
let { icon }: Props = $props();
let iconComponent = $derived(import(`./svg/${icon}.svelte`));
</script>
<span class="icon" aria-hidden="true">
{#await iconComponent then { default: component }}
<svelte:component this={component} />
{@const SvelteComponent = component}
<SvelteComponent />
{/await}
</span>

View File

@ -3,8 +3,12 @@
import type { OAuth2ClientInfo, UserSession } from '$lib/types';
import AvatarCard from '../avatar/AvatarCard.svelte';
export let user: UserSession;
export let client: OAuth2ClientInfo;
interface Props {
user: UserSession;
client: OAuth2ClientInfo;
}
let { user, client }: Props = $props();
</script>
<div class="user-client-wrapper">

View File

@ -2,7 +2,11 @@
import { t } from '$lib/i18n';
import type { OAuth2ClientInfo } from '$lib/types';
export let client: OAuth2ClientInfo;
interface Props {
client: OAuth2ClientInfo;
}
let { client }: Props = $props();
</script>
<div class="scope">

View File

@ -32,3 +32,17 @@ export const waitIsTruthy = (check: () => boolean, checkInterval = 500, checkTim
}, checkInterval);
});
};
/**
* Only handle event if the event target is current element
* @param fn True event handler
* @returns Wrapped event handler
*/
export function self<TFn extends (event: Event, ...args: Array<unknown>) => void>(fn: TFn) {
return function (this: EventTarget, ...args: Parameters<TFn>) {
const event = args[0];
if (event.target === this) {
fn?.apply(this, args);
}
};
}

View File

@ -10,7 +10,7 @@
<h1>{$page.status}</h1>
<p>{$page.error?.message}</p>
{#if $page.status !== 404}
<Button on:click={() => invalidateAll()} variant="link">Go back</Button>
<Button onclick={() => invalidateAll()} variant="link">Go back</Button>
{/if}
</div>
</MainContainer>

View File

@ -3,6 +3,11 @@
import { t } from '$lib/i18n';
import { forwardInitialTheme } from '$lib/theme-mode';
import '../app.css';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
forwardInitialTheme();
</script>
@ -15,4 +20,4 @@
/>
</svelte:head>
<slot></slot>
{@render children?.()}

View File

@ -23,15 +23,19 @@
import ThemeButton from '$lib/components/ThemeButton.svelte';
import { hasPrivileges } from '$lib/utils';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let internalErrors: string[] = [];
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
$: adminButton = hasPrivileges(data.privileges, ['admin', 'self:oauth2']);
let { data, form }: Props = $props();
let usernameRef: HTMLInputElement;
let displayRef: HTMLInputElement;
let internalErrors: string[] = $state([]);
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
let adminButton = $derived(hasPrivileges(data.privileges, ['admin', 'self:oauth2']));
let usernameRef: HTMLInputElement = $state()!;
let displayRef: HTMLInputElement = $state()!;
let showAvatarModal = writable(false);
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
@ -92,7 +96,7 @@
<input name="challenge" value={form.otpRequired} type="hidden" />
<FormControl>
<label for="form-otpCode">{$t('account.login.otpCode')}</label>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input id="form-otpCode" name="otpCode" autocomplete="off" autofocus />
</FormControl>
</FormSection>
@ -180,38 +184,43 @@
</ViewColumn>
</div>
<ViewColumn slot="side">
<div>
<h3>{$t('account.avatar.title')}</h3>
<AvatarCard src={`/api/avatar/${data.user.uuid}?t=${data.updateRef}`} alt={data.user.name}>
<ViewColumn>
<div>
<Button variant="primary" on:click={() => ($showAvatarModal = true)}
>{$t('account.avatar.change')}</Button
>
</div>
{#if data.hasAvatar}
<ActionButton action="?/removeAvatar">{$t('account.avatar.remove')}</ActionButton>
{/if}
</ViewColumn>
</AvatarCard>
</div>
{#snippet side()}
<ViewColumn>
<div>
<h3>{$t('account.avatar.title')}</h3>
<AvatarCard
src={`/api/avatar/${data.user.uuid}?t=${data.updateRef}`}
alt={data.user.name}
>
<ViewColumn>
<div>
<Button variant="primary" onclick={() => ($showAvatarModal = true)}
>{$t('account.avatar.change')}</Button
>
</div>
{#if data.hasAvatar}
<ActionButton action="?/removeAvatar">{$t('account.avatar.remove')}</ActionButton>
{/if}
</ViewColumn>
</AvatarCard>
</div>
<div>
<h3>{$t('account.otp.title')}</h3>
{#if data.otpEnabled}
<p>{$t('account.otp.enabled')}</p>
{:else}
<p>{$t('account.otp.disabled')}</p>
{/if}
<a href="/account/two-factor">{$t('common.manage')}</a>
</div>
<div>
<h3>{$t('account.otp.title')}</h3>
{#if data.otpEnabled}
<p>{$t('account.otp.enabled')}</p>
{:else}
<p>{$t('account.otp.disabled')}</p>
{/if}
<a href="/account/two-factor">{$t('common.manage')}</a>
</div>
<div>
<h3>{$t('account.authorizations.title')}</h3>
<a href="/account/authorizations">{$t('common.manage')}</a>
</div>
</ViewColumn>
<div>
<h3>{$t('account.authorizations.title')}</h3>
<a href="/account/authorizations">{$t('common.manage')}</a>
</div>
</ViewColumn>
{/snippet}
</SplitView>
</MainContainer>

View File

@ -10,7 +10,11 @@
import { t } from '$lib/i18n';
import type { PageData } from './$types';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>

View File

@ -11,8 +11,12 @@
import { t } from '$lib/i18n';
import type { ActionData, PageData } from './$types';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let { data, form }: Props = $props();
</script>
<svelte:head>

View File

@ -12,8 +12,12 @@
import FormControl from '$lib/components/form/FormControl.svelte';
import FormErrors from '$lib/components/form/FormErrors.svelte';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let { data, form }: Props = $props();
</script>
<svelte:head>

View File

@ -13,8 +13,12 @@
import TitleRow from '$lib/components/container/TitleRow.svelte';
import ThemeButton from '$lib/components/ThemeButton.svelte';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let { data, form }: Props = $props();
</script>
<svelte:head>
@ -46,7 +50,7 @@
<input name="challenge" value={form.otpRequired} type="hidden" />
<FormControl>
<label for="login-otpCode">{$t('account.login.otpCode')}</label>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input id="login-otpCode" name="otpCode" autocomplete="off" autofocus />
</FormControl>
</FormSection>

View File

@ -13,16 +13,20 @@
import { t } from '$lib/i18n';
import type { ActionData, PageData, SubmitFunction } from './$types';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let internalErrors: string[] = [];
let submitted = false;
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
$: actionUrl = data.setter
let { data, form }: Props = $props();
let internalErrors: string[] = $state([]);
let submitted = $state(false);
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
let actionUrl = $derived(data.setter
? `?/setPassword&token=${$page.url.searchParams.get('token')}`
: '?/sendEmail';
$: pageTitle = data.setter ? 'setNewPassword' : 'resetPassword';
: '?/sendEmail');
let pageTitle = $derived(data.setter ? 'setNewPassword' : 'resetPassword');
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
internalErrors.length = 0;

View File

@ -9,7 +9,11 @@
import OAuth2AuthorizeCard from '$lib/components/oauth2/OAuth2AuthorizeCard.svelte';
import OAuth2ScopesCard from '$lib/components/oauth2/OAuth2ScopesCard.svelte';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>
@ -26,7 +30,7 @@
>{data.error}: {data.error_description}</code
></Alert
>
<Button on:click={() => history?.back()} variant="link">{$t('common.back')}</Button>
<Button onclick={() => history?.back()} variant="link">{$t('common.back')}</Button>
</ColumnView>
{/if}

View File

@ -15,12 +15,16 @@
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
import HCaptcha from '$lib/components/form/HCaptcha.svelte';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let internalErrors: string[] = [];
let submitted = false;
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
let { data, form }: Props = $props();
let internalErrors: string[] = $state([]);
let submitted = $state(false);
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
internalErrors.length = 0;

View File

@ -4,9 +4,14 @@
import AdminSidebar from '$lib/components/admin/AdminSidebar.svelte';
import { tick } from 'svelte';
import type { PageData } from './$types';
export let data: PageData;
interface Props {
data: PageData;
children?: import('svelte').Snippet;
}
let container: HTMLElement;
let { data, children }: Props = $props();
let container: HTMLElement = $state();
// Reset container scroll.
afterNavigate(async ({ type }) => {
@ -23,7 +28,7 @@
<AdminSidebar user={data.user} />
<main bind:this={container}>
<slot />
{@render children?.()}
</main>
</div>
</div>

View File

@ -10,7 +10,11 @@
import Button from '$lib/components/Button.svelte';
import AdminAuditCard from '$lib/components/admin/AdminAuditCard.svelte';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>

View File

@ -9,7 +9,11 @@
import FormControl from '$lib/components/form/FormControl.svelte';
import { page } from '$app/stores';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>

View File

@ -18,27 +18,33 @@
import { env } from '$env/dynamic/public';
import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let { data, form }: Props = $props();
let showAvatarModal = writable(false);
let secret = false;
let addingUrl = false;
let addingPrivilege = false;
let addingManager = false;
let secret = $state(false);
let addingUrl = $state(false);
let addingPrivilege = $state(false);
let addingManager = $state(false);
$: noRedirects = !data.details.urls.some(({ type }) => type === 'redirect_uri');
$: availableUrls = data.availableUrls.filter((type) => {
// Can have up to five redirect URIs, only one of other types
const countOfType = data.details.urls.filter(({ type: subType }) => type === subType).length;
if (type === 'redirect_uri') {
return countOfType < OAUTH2_MAX_REDIRECTS;
}
return countOfType < OAUTH2_MAX_URLS;
});
$: splitScopes = data.details.scope?.split(' ') || [];
$: splitGrants = data.details.grants?.split(' ') || [];
$: uuidPrefix = data.details.client_id.split('-')[0] + ':';
let noRedirects = $derived(!data.details.urls.some(({ type }) => type === 'redirect_uri'));
let availableUrls = $derived(
data.availableUrls.filter((type) => {
// Can have up to five redirect URIs, only one of other types
const countOfType = data.details.urls.filter(({ type: subType }) => type === subType).length;
if (type === 'redirect_uri') {
return countOfType < OAUTH2_MAX_REDIRECTS;
}
return countOfType < OAUTH2_MAX_URLS;
})
);
let splitScopes = $derived(data.details.scope?.split(' ') || []);
let splitGrants = $derived(data.details.grants?.split(' ') || []);
let uuidPrefix = $derived(data.details.client_id.split('-')[0] + ':');
</script>
<svelte:head>
@ -56,7 +62,7 @@
<AvatarCard src={`/api/avatar/client/${data.details.client_id}?t=${data.renderrt}`}>
<ColumnView>
<div>
<Button variant="primary" on:click={() => ($showAvatarModal = true)}
<Button variant="primary" onclick={() => ($showAvatarModal = true)}
>{$t('admin.oauth2.avatar.change')}</Button
>
</div>
@ -88,7 +94,7 @@
{#if secret}
<input readonly id="client-secret" value={data.details.client_secret} />
{:else}
<Button on:click={() => (secret = true)}>{$t('admin.oauth2.reveal')}</Button>
<Button onclick={() => (secret = true)}>{$t('admin.oauth2.reveal')}</Button>
{/if}
</FormControl>
@ -99,7 +105,7 @@
id="client-description"
value={data.details.description}
rows="3"
/>
></textarea>
</FormControl>
<FormControl>
@ -175,7 +181,7 @@
{#if !addingUrl && availableUrls.length}
<div>
<Button variant="link" on:click={() => (addingUrl = true)}
<Button variant="link" onclick={() => (addingUrl = true)}
>+ {$t('admin.oauth2.urls.add')}</Button
>
</div>
@ -200,7 +206,7 @@
<ButtonRow>
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
<Button variant="link" on:click={() => (addingUrl = false)}
<Button variant="link" onclick={() => (addingUrl = false)}
>{$t('common.cancel')}</Button
>
</ButtonRow>
@ -227,7 +233,7 @@
{#if !addingPrivilege}
<div>
<Button variant="link" on:click={() => (addingPrivilege = true)}
<Button variant="link" onclick={() => (addingPrivilege = true)}
>+ {$t('admin.oauth2.privileges.add')}</Button
>
</div>
@ -245,7 +251,7 @@
<ButtonRow>
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
<Button variant="link" on:click={() => (addingPrivilege = false)}
<Button variant="link" onclick={() => (addingPrivilege = false)}
>{$t('common.cancel')}</Button
>
</ButtonRow>
@ -331,7 +337,7 @@
<Button type="submit" variant="primary"
>{$t('admin.oauth2.managers.invite')}</Button
>
<Button variant="link" on:click={() => (addingManager = false)}
<Button variant="link" onclick={() => (addingManager = false)}
>{$t('common.cancel')}</Button
>
</ButtonRow>
@ -340,7 +346,7 @@
</form>
{:else}
<div>
<Button variant="link" on:click={() => (addingManager = true)}
<Button variant="link" onclick={() => (addingManager = true)}
>+ {$t('admin.oauth2.managers.add')}</Button
>
</div>

View File

@ -8,7 +8,11 @@
import { t } from '$lib/i18n';
import type { PageData } from './$types';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>

View File

@ -9,7 +9,11 @@
import { env } from '$env/dynamic/public';
import SplitView from '$lib/components/container/SplitView.svelte';
export let form: ActionData;
interface Props {
form: ActionData;
}
let { form }: Props = $props();
</script>
<svelte:head>
@ -42,7 +46,7 @@
<FormControl>
<label for="client-description">{$t('admin.oauth2.description')}</label>
<textarea name="description" id="client-description" rows="3" />
<textarea name="description" id="client-description" rows="3"></textarea>
</FormControl>
</FormSection>

View File

@ -8,7 +8,11 @@
import ColumnView from '$lib/components/container/ColumnView.svelte';
import { page } from '$app/stores';
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>

View File

@ -50,6 +50,7 @@ export const actions = {
username: `stub${stubName}`,
display_name: `Stub ${stubName}`,
email: `${stubName}@uuid-stub.target`,
password: null,
activated: 0,
updated_at: new Date()
});

View File

@ -14,8 +14,12 @@
import ActionButton from '$lib/components/ActionButton.svelte';
import Alert from '$lib/components/Alert.svelte';
export let data: PageData;
export let form: ActionData;
interface Props {
data: PageData;
form: ActionData;
}
let { data, form }: Props = $props();
</script>
<svelte:head>

View File

@ -1,6 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import mkcert from 'vite-plugin-mkcert';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit(), mkcert()],
server: {
proxy: {}
}
});