Migrate to Svelte 5 and upgrade dependencies
This commit is contained in:
parent
4ab7145ca5
commit
13fc448487
2502
package-lock.json
generated
2502
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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}><<</Button>
|
||||
<Button variant="primary" on:click={transferToCurrent}>>></Button>
|
||||
<Button variant="primary" onclick={transferToAvailable}><<</Button>
|
||||
<Button variant="primary" onclick={transferToCurrent}>>></Button>
|
||||
</ColumnView>
|
||||
|
||||
<ColumnView>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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?.()}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
</< |