mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-11 06:34:19 +00:00
feat: add support for translations (#349)
Co-authored-by: Kyle Mendell <kmendell@outlook.com> Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="%lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/api/application-configuration/favicon" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { AxiosError } from 'axios';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
@@ -9,7 +11,16 @@ import { decodeJwt } from 'jose';
|
||||
// this is still secure as process will just be undefined in the browser
|
||||
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Handle to use the paraglide middleware
|
||||
const paraglideHandle: Handle = ({ event, resolve }) => {
|
||||
return paraglideMiddleware(event.request, ({ locale }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%lang%', locale)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const authenticationHandle: Handle = async ({ event, resolve }) => {
|
||||
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||
|
||||
const isUnauthenticatedOnlyPath = event.url.pathname.startsWith('/login') || event.url.pathname.startsWith('/lc')
|
||||
@@ -43,6 +54,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(paraglideHandle, authenticationHandle);
|
||||
|
||||
export const handleError: HandleServerError = async ({ error, message, status }) => {
|
||||
if (error instanceof AxiosError) {
|
||||
message = error.response?.data.error || message;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Button from './ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
items,
|
||||
@@ -93,7 +94,7 @@
|
||||
'relative z-50 mb-4 max-w-sm',
|
||||
items.data.length == 0 && searchValue == '' && 'hidden'
|
||||
)}
|
||||
placeholder={'Search...'}
|
||||
placeholder={m.search()}
|
||||
type="text"
|
||||
oninput={(e) => onSearch((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
@@ -102,7 +103,7 @@
|
||||
{#if items.data.length === 0 && searchValue === ''}
|
||||
<div class="my-5 flex flex-col items-center">
|
||||
<Empty class="text-muted-foreground h-20" />
|
||||
<p class="text-muted-foreground mt-3 text-sm">No items found</p>
|
||||
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||
@@ -166,7 +167,7 @@
|
||||
|
||||
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="text-sm font-medium">Items per page</p>
|
||||
<p class="text-sm font-medium">{m.items_per_page()}</p>
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: items.pagination.itemsPerPage.toString(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -55,7 +56,7 @@
|
||||
<Card.Description>{description}</Card.Description>
|
||||
{/if}
|
||||
</div>
|
||||
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label="Expand card">
|
||||
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
|
||||
<LucideChevronDown
|
||||
class={cn(
|
||||
'h-5 w-5 transition-transform duration-200',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import ConfirmDialog from './confirm-dialog.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
export const confirmDialogStore = writable({
|
||||
open: false,
|
||||
title: '',
|
||||
message: '',
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
label: m.confirm(),
|
||||
destructive: false,
|
||||
action: () => {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideCheck } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -31,9 +32,9 @@
|
||||
<Tooltip.Trigger class="text-start" tabindex={-1} onclick={onClick}>{@render children()}</Tooltip.Trigger>
|
||||
<Tooltip.Content onclick={copyToClipboard}>
|
||||
{#if copied}
|
||||
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> Copied</span>
|
||||
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> {m.copied()}</span>
|
||||
{:else}
|
||||
<span>Click to copy</span>
|
||||
<span>{m.click_to_copy()}</span>
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideXCircle } from 'lucide-svelte';
|
||||
|
||||
let { message, showButton = true }: { message: string; showButton?: boolean } = $props();
|
||||
@@ -7,9 +8,9 @@
|
||||
|
||||
<div class="mt-[20%] flex flex-col items-center">
|
||||
<LucideXCircle class="h-12 w-12 text-muted-foreground" />
|
||||
<h1 class="mt-3 text-2xl font-semibold">Something went wrong</h1>
|
||||
<h1 class="mt-3 text-2xl font-semibold">{m.something_went_wrong()}</h1>
|
||||
<p class="text-muted-foreground">{message}</p>
|
||||
{#if showButton}
|
||||
<Button size="sm" class="mt-5" href="/">Go back to home</Button>
|
||||
<Button size="sm" class="mt-5" href="/">{m.go_back_to_home()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import AutoCompleteInput from './auto-complete-input.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
customClaims = $bindable(),
|
||||
@@ -41,15 +42,15 @@
|
||||
{#each customClaims as _, i}
|
||||
<div class="flex gap-x-2">
|
||||
<AutoCompleteInput
|
||||
placeholder="Key"
|
||||
placeholder={m.key()}
|
||||
suggestions={filteredSuggestions}
|
||||
bind:value={customClaims[i].key}
|
||||
/>
|
||||
<Input placeholder="Value" bind:value={customClaims[i].value} />
|
||||
<Input placeholder={m.value()} bind:value={customClaims[i].value} />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label="Remove custom claim"
|
||||
aria-label={m.remove_custom_claim()}
|
||||
on:click={() => (customClaims = customClaims.filter((_, index) => index !== i))}
|
||||
>
|
||||
<LucideMinus class="h-4 w-4" />
|
||||
@@ -69,7 +70,7 @@
|
||||
on:click={() => (customClaims = [...customClaims, { key: '', value: '' }])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
{customClaims.length === 0 ? 'Add custom claim' : 'Add another'}
|
||||
{customClaims.length === 0 ? m.add_custom_claim() : m.add_another()}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Calendar } from '$lib/components/ui/calendar';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import {
|
||||
CalendarDate,
|
||||
@@ -30,7 +32,7 @@
|
||||
open = false;
|
||||
}
|
||||
|
||||
const df = new DateFormatter('en-US', {
|
||||
const df = new DateFormatter(getLocale(), {
|
||||
dateStyle: 'long'
|
||||
});
|
||||
</script>
|
||||
@@ -44,7 +46,7 @@
|
||||
builders={[builder]}
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{date ? df.format(date.toDate(getLocalTimeZone())) : 'Select a date'}
|
||||
{date ? df.format(date.toDate(getLocalTimeZone())) : m.select_a_date()}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0" align="start">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
import type { buttonVariants } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -21,7 +22,7 @@
|
||||
{#if restProps.children}
|
||||
{@render restProps.children()}
|
||||
{:else}
|
||||
Select File
|
||||
{m.select_file()}
|
||||
{/if}
|
||||
</button>
|
||||
<input {id} {...restProps} type="file" class="hidden" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
userId,
|
||||
@@ -40,11 +41,10 @@
|
||||
|
||||
function onReset() {
|
||||
openConfirmDialog({
|
||||
title: 'Reset profile picture?',
|
||||
message:
|
||||
'This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?',
|
||||
title: m.reset_profile_picture_question(),
|
||||
message: m.this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default(),
|
||||
confirm: {
|
||||
label: 'Reset',
|
||||
label: m.reset(),
|
||||
action: async () => {
|
||||
isLoading = true;
|
||||
await resetCallback().catch();
|
||||
@@ -58,16 +58,16 @@
|
||||
<div class="flex gap-5">
|
||||
<div class="flex w-full flex-col justify-between gap-5 sm:flex-row">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">Profile Picture</h3>
|
||||
<h3 class="text-xl font-semibold">{m.profile_picture()}</h3>
|
||||
{#if isLdapUser}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
The profile picture is managed by the LDAP server and cannot be changed here.
|
||||
{m.profile_picture_is_managed_by_ldap_server()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
Click on the profile picture to upload a custom one from your files.
|
||||
{m.click_profile_picture_to_upload_custom()}
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">The image should be in PNG or JPEG format.</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{m.image_should_be_in_format()}</p>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -77,7 +77,7 @@
|
||||
disabled={isLoading || isLdapUser}
|
||||
>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
Reset to default
|
||||
{m.reset_to_default()}
|
||||
</Button>
|
||||
</div>
|
||||
{#if isLdapUser}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { LucideLogOut, LucideUser } from 'lucide-svelte';
|
||||
@@ -32,10 +33,10 @@
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item href="/settings/account"
|
||||
><LucideUser class="mr-2 h-4 w-4" /> My Account</DropdownMenu.Item
|
||||
><LucideUser class="mr-2 h-4 w-4" /> {m.my_account()}</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item on:click={logout}
|
||||
><LucideLogOut class="mr-2 h-4 w-4" /> Logout</DropdownMenu.Item
|
||||
><LucideLogOut class="mr-2 h-4 w-4" /> {m.logout()}</DropdownMenu.Item
|
||||
>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import Logo from '../logo.svelte';
|
||||
@@ -8,7 +8,7 @@
|
||||
const authUrls = [/^\/authorize$/, /^\/login(?:\/.*)?$/, /^\/logout$/];
|
||||
|
||||
let isAuthPage = $derived(
|
||||
!$page.error && authUrls.some((pattern) => pattern.test($page.url.pathname))
|
||||
!page.error && authUrls.some((pattern) => pattern.test(page.url.pathname))
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
</h1>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $userStore?.id}
|
||||
<HeaderAvatar />
|
||||
{/if}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
{#if $userStore?.id}
|
||||
<HeaderAvatar />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
children,
|
||||
@@ -29,7 +30,7 @@
|
||||
)}`}
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -38,7 +39,7 @@
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
class="h-screen w-[calc(100vw-650px)] rounded-l-[60px] object-cover"
|
||||
alt="Login background"
|
||||
alt={m.login_background()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
)}`}
|
||||
class="text-muted-foreground mt-7 flex justify-center text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
{/if}
|
||||
</Card.CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { mode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -7,4 +8,4 @@
|
||||
const isDarkMode = $derived($mode === 'dark');
|
||||
</script>
|
||||
|
||||
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt="Logo" />
|
||||
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt={m.logo()} />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
|
||||
@@ -17,14 +18,14 @@
|
||||
const userService = new UserService();
|
||||
|
||||
let oneTimeLink: string | null = $state(null);
|
||||
let selectedExpiration: keyof typeof availableExpirations = $state('1 hour');
|
||||
let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour());
|
||||
|
||||
let availableExpirations = {
|
||||
'1 hour': 60 * 60,
|
||||
'12 hours': 60 * 60 * 12,
|
||||
'1 day': 60 * 60 * 24,
|
||||
'1 week': 60 * 60 * 24 * 7,
|
||||
'1 month': 60 * 60 * 24 * 30
|
||||
[m.one_hour()]: 60 * 60,
|
||||
[m.twelve_hours()]: 60 * 60 * 12,
|
||||
[m.one_day()]: 60 * 60 * 24,
|
||||
[m.one_week()]: 60 * 60 * 24 * 7,
|
||||
[m.one_month()]: 60 * 60 * 24 * 30
|
||||
};
|
||||
|
||||
async function createOneTimeAccessToken() {
|
||||
@@ -48,14 +49,14 @@
|
||||
<Dialog.Root open={!!userId} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Login Code</Dialog.Title>
|
||||
<Dialog.Title>{m.login_code()}</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Create a login code that the user can use to sign in without a passkey once.</Dialog.Description
|
||||
>{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description
|
||||
>
|
||||
</Dialog.Header>
|
||||
{#if oneTimeLink === null}
|
||||
<div>
|
||||
<Label for="expiration">Expiration</Label>
|
||||
<Label for="expiration">{m.expiration()}</Label>
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: Object.keys(availableExpirations)[0],
|
||||
@@ -75,10 +76,10 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Button onclick={() => createOneTimeAccessToken()} disabled={!selectedExpiration}>
|
||||
Generate Code
|
||||
{m.generate_code()}
|
||||
</Button>
|
||||
{:else}
|
||||
<Label for="login-code" class="sr-only">Login Code</Label>
|
||||
<Label for="login-code" class="sr-only">{m.login_code()}</Label>
|
||||
<Input id="login-code" value={oneTimeLink} readonly />
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
@@ -34,7 +35,7 @@
|
||||
items={groups}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (groups = await userGroupService.list(o))}
|
||||
columns={[{ label: 'Name', sortColumn: 'friendlyName' }]}
|
||||
columns={[{ label: m.name(), sortColumn: 'friendlyName' }]}
|
||||
bind:selectedIds={selectedGroupIds}
|
||||
{selectionDisabled}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import Logo from './logo.svelte';
|
||||
</script>
|
||||
|
||||
@@ -6,8 +7,8 @@
|
||||
<div class="bg-muted mx-auto rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Browser unsupported</p>
|
||||
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.browser_unsupported()}</p>
|
||||
<p class="text-muted-foreground mt-3">
|
||||
This browser doesn't support passkeys. Please or use a alternative sign in method.
|
||||
{m.this_browser_does_not_support_passkeys()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { setLocale } from '$lib/paraglide/runtime';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const userStore = writable<User | null>(null);
|
||||
|
||||
const setUser = (user: User) => {
|
||||
if (user.locale) {
|
||||
setLocale(user.locale, { reload: false });
|
||||
}
|
||||
userStore.set(user);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Locale } from '$lib/paraglide/runtime';
|
||||
import type { CustomClaim } from './custom-claim.type';
|
||||
import type { UserGroup } from './user-group.type';
|
||||
|
||||
@@ -10,6 +11,7 @@ export type User = {
|
||||
isAdmin: boolean;
|
||||
userGroups: UserGroup[];
|
||||
customClaims: CustomClaim[];
|
||||
locale?: Locale;
|
||||
ldapId?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { WebAuthnError } from '@simplewebauthn/browser';
|
||||
import { AxiosError } from 'axios';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export function getAxiosErrorMessage(
|
||||
e: unknown,
|
||||
defaultMessage: string = 'An unknown error occurred'
|
||||
defaultMessage: string = m.an_unknown_error_occurred()
|
||||
) {
|
||||
let message = defaultMessage;
|
||||
if (e instanceof AxiosError) {
|
||||
@@ -13,29 +14,29 @@ export function getAxiosErrorMessage(
|
||||
return message;
|
||||
}
|
||||
|
||||
export function axiosErrorToast(e: unknown, defaultMessage: string = 'An unknown error occurred') {
|
||||
export function axiosErrorToast(e: unknown, defaultMessage: string = m.an_unknown_error_occurred()) {
|
||||
const message = getAxiosErrorMessage(e, defaultMessage);
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
export function getWebauthnErrorMessage(e: unknown) {
|
||||
const errors = {
|
||||
ERROR_CEREMONY_ABORTED: 'The authentication process was aborted',
|
||||
ERROR_AUTHENTICATOR_GENERAL_ERROR: 'An error occurred with the authenticator',
|
||||
ERROR_CEREMONY_ABORTED: m.authentication_process_was_aborted(),
|
||||
ERROR_AUTHENTICATOR_GENERAL_ERROR: m.error_occurred_with_authenticator(),
|
||||
ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT:
|
||||
'The authenticator does not support discoverable credentials',
|
||||
m.authenticator_does_not_support_discoverable_credentials(),
|
||||
ERROR_AUTHENTICATOR_MISSING_RESIDENT_KEY_SUPPORT:
|
||||
'The authenticator does not support resident keys',
|
||||
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: 'This passkey was previously registered',
|
||||
m.authenticator_does_not_support_resident_keys(),
|
||||
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: m.passkey_was_previously_registered(),
|
||||
ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG:
|
||||
'The authenticator does not support any of the requested algorithms'
|
||||
m.authenticator_does_not_support_any_of_the_requested_algorithms()
|
||||
};
|
||||
|
||||
let message = 'An unknown error occurred';
|
||||
let message = m.an_unknown_error_occurred();
|
||||
if (e instanceof WebAuthnError && e.code in errors) {
|
||||
message = errors[e.code as keyof typeof errors];
|
||||
} else if (e instanceof WebAuthnError && e?.message.includes('timed out')) {
|
||||
message = 'The authenticator timed out';
|
||||
message = m.authenticator_timed_out();
|
||||
} else if (e instanceof AxiosError && e.response?.data.error) {
|
||||
message = e.response?.data.error;
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Error from '$lib/components/error.svelte';
|
||||
import Header from '$lib/components/header/header.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
@@ -30,10 +31,7 @@
|
||||
</script>
|
||||
|
||||
{#if !appConfig}
|
||||
<Error
|
||||
message="A critical error occurred. Please contact your administrator."
|
||||
showButton={false}
|
||||
/>
|
||||
<Error message={m.critical_error_occurred_contact_administrator()} showButton={false} />
|
||||
{:else}
|
||||
<Header />
|
||||
{@render children()}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import ClientProviderImages from './components/client-provider-images.svelte';
|
||||
import ScopeItem from './components/scope-item.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
const oidService = new OidcService();
|
||||
@@ -77,15 +78,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign in to {client.name}</title>
|
||||
<title>{m.sign_in_to({name: client.name})}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if client == null}
|
||||
<p>Client not found</p>
|
||||
<p>{m.client_not_found()}</p>
|
||||
{:else}
|
||||
<SignInWrapper showAlternativeSignInMethodButton>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.sign_in_to({name: client.name})}</h1>
|
||||
{#if errorMessage}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
{errorMessage}.
|
||||
@@ -93,34 +94,36 @@
|
||||
{/if}
|
||||
{#if !authorizationRequired && !errorMessage}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
Do you want to sign in to <b>{client.name}</b> with your
|
||||
<b>{$appConfigStore.appName}</b> account?
|
||||
{@html m.do_you_want_to_sign_in_to_client_with_your_app_name_account({
|
||||
client: client.name,
|
||||
appName: $appConfigStore.appName
|
||||
})}
|
||||
</p>
|
||||
{:else if authorizationRequired}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
<Card.Root class="mb-10 mt-6">
|
||||
<Card.Header class="pb-5">
|
||||
<p class="text-muted-foreground text-start">
|
||||
<b>{client.name}</b> wants to access the following information:
|
||||
{@html m.client_wants_to_access_the_following_information({ client: client.name })}
|
||||
</p>
|
||||
</Card.Header>
|
||||
<Card.Content data-testid="scopes">
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if scope!.includes('email')}
|
||||
<ScopeItem icon={LucideMail} name="Email" description="View your email address" />
|
||||
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
|
||||
{/if}
|
||||
{#if scope!.includes('profile')}
|
||||
<ScopeItem
|
||||
icon={LucideUser}
|
||||
name="Profile"
|
||||
description="View your profile information"
|
||||
name={m.profile()}
|
||||
description={m.view_your_profile_information()}
|
||||
/>
|
||||
{/if}
|
||||
{#if scope!.includes('groups')}
|
||||
<ScopeItem
|
||||
icon={LucideUsers}
|
||||
name="Groups"
|
||||
description="View the groups you are a member of"
|
||||
name={m.groups()}
|
||||
description={m.view_the_groups_you_are_a_member_of()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -129,11 +132,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex w-full justify-stretch gap-2">
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">{m.cancel()}</Button>
|
||||
{#if !errorMessage}
|
||||
<Button class="w-full" {isLoading} on:click={authorize}>Sign in</Button>
|
||||
<Button class="w-full" {isLoading} on:click={authorize}>{m.sign_in()}</Button>
|
||||
{:else}
|
||||
<Button class="w-full" on:click={() => (errorMessage = null)}>Try again</Button>
|
||||
<Button class="w-full" on:click={() => (errorMessage = null)}>{m.try_again()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import CheckmarkAnimated from '$lib/icons/checkmark-animated.svelte';
|
||||
import ConnectArrow from '$lib/icons/connect-arrow.svelte';
|
||||
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { OidcClientMetaData } from '$lib/types/oidc.type';
|
||||
|
||||
const {
|
||||
@@ -61,7 +62,7 @@
|
||||
class="h-10 w-10"
|
||||
src="/api/oidc/clients/{client.id}/logo"
|
||||
draggable={false}
|
||||
alt="Client Logo"
|
||||
alt={m.client_logo()}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-10 w-10 items-center justify-center text-3xl font-bold">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from './components/login-logo-error-success-indicator.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
const webauthnService = new WebAuthnService();
|
||||
|
||||
let isLoading = $state(false);
|
||||
@@ -32,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
<title>{m.sign_in()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper showAlternativeSignInMethodButton>
|
||||
@@ -40,18 +41,18 @@
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
Sign in to {$appConfigStore.appName}
|
||||
{m.sign_in_to_appname({ appName: $appConfigStore.appName})}
|
||||
</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}. Please try to sign in again.
|
||||
{error}. {m.please_try_to_sign_in_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
Authenticate yourself with your passkey to access the admin panel.
|
||||
{m.authenticate_yourself_with_your_passkey_to_access_the_admin_panel()}
|
||||
</p>
|
||||
{/if}
|
||||
<Button class="mt-10" {isLoading} on:click={authenticate}
|
||||
>{error ? 'Try again' : 'Authenticate'}</Button
|
||||
>{error ? m.try_again() : m.authenticate()}</Button
|
||||
>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import { LucideChevronRight, LucideMail, LucideRectangleEllipsis } from 'lucide-svelte';
|
||||
|
||||
const methods = [
|
||||
{
|
||||
icon: LucideRectangleEllipsis,
|
||||
title: 'Login Code',
|
||||
description: 'Enter a login code to sign in.',
|
||||
title: m.login_code(),
|
||||
description: m.enter_a_login_code_to_sign_in(),
|
||||
href: '/login/alternative/code'
|
||||
}
|
||||
];
|
||||
@@ -19,15 +20,15 @@
|
||||
if ($appConfigStore.emailOneTimeAccessEnabled) {
|
||||
methods.push({
|
||||
icon: LucideMail,
|
||||
title: 'Email Login',
|
||||
description: 'Request a login code via email.',
|
||||
title: m.email_login(),
|
||||
description: m.request_a_login_code_via_email(),
|
||||
href: '/login/alternative/email'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
<title>{m.sign_in()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
@@ -35,9 +36,9 @@
|
||||
<div class="bg-muted mx-auto rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Alternative Sign In</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.alternative_sign_in()}</h1>
|
||||
<p class="text-muted-foreground mt-3">
|
||||
If you dont't have access to your passkey, you can sign in using one of the following methods.
|
||||
{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}
|
||||
</p>
|
||||
<div class="mt-5 flex flex-col gap-3">
|
||||
{#each methods as method}
|
||||
@@ -59,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
<a class="text-muted-foreground mt-5 text-xs" href={'/login' + page.url.search}
|
||||
>Use your passkey instead?</a
|
||||
>{m.use_your_passkey_instead()}</a
|
||||
>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let code = $state(data.code ?? '');
|
||||
@@ -26,7 +27,7 @@
|
||||
try {
|
||||
goto(data.redirect);
|
||||
} catch (e) {
|
||||
error = 'Invalid redirect URL';
|
||||
error = m.invalid_redirect_url();
|
||||
}
|
||||
} catch (e) {
|
||||
error = getAxiosErrorMessage(e);
|
||||
@@ -43,20 +44,20 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Login Code</title>
|
||||
<title>{m.login_code()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">Login Code</h1>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.login_code()}</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2">Enter the code you received to sign in.</p>
|
||||
<p class="text-muted-foreground mt-2">{m.enter_the_code_you_received_to_sign_in()}</p>
|
||||
{/if}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
@@ -65,10 +66,10 @@
|
||||
}}
|
||||
class="w-full max-w-[450px]"
|
||||
>
|
||||
<Input id="Email" class="mt-7" placeholder="Code" bind:value={code} type="text" />
|
||||
<Input id="Email" class="mt-7" placeholder={m.code()} bind:value={code} type="text" />
|
||||
<div class="mt-8 flex justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={"/login/alternative" + page.url.search}>Go back</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>Submit</Button>
|
||||
<Button variant="secondary" class="w-full" href={"/login/alternative" + page.url.search}>{m.go_back()}</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>{m.submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
@@ -21,38 +22,38 @@
|
||||
await userService
|
||||
.requestOneTimeAccessEmail(email, data.redirect)
|
||||
.then(() => (success = true))
|
||||
.catch((e) => (error = e.response?.data.error || 'An unknown error occurred'));
|
||||
.catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred()));
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email Login</title>
|
||||
<title>{m.email_login()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator {success} error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Email Login</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.email_login()}</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href="/">Go back</Button>
|
||||
<Button class="w-full" onclick={() => (error = undefined)}>Try again</Button>
|
||||
<Button variant="secondary" class="w-full" href="/">{m.go_back()}</Button>
|
||||
<Button class="w-full" onclick={() => (error = undefined)}>{m.try_again()}</Button>
|
||||
</div>
|
||||
{:else if success}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
An email has been sent to the provided email, if it exists in the system.
|
||||
{m.an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system()}
|
||||
</p>
|
||||
<div class="mt-8 flex w-full justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={'/login/alternative' + page.url.search}
|
||||
>Go back</Button
|
||||
>{m.go_back()}</Button
|
||||
>
|
||||
<Button class="w-full" href={'/login/alternative/code' + page.url.search}>Enter code</Button>
|
||||
<Button class="w-full" href={'/login/alternative/code' + page.url.search}>{m.enter_code()}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<form
|
||||
@@ -63,14 +64,14 @@
|
||||
class="w-full max-w-[450px]"
|
||||
>
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
Enter your email address to receive an email with a login code.
|
||||
{m.enter_your_email_address_to_receive_an_email_with_a_login_code()}
|
||||
</p>
|
||||
<Input id="Email" class="mt-7" placeholder="Your email" bind:value={email} />
|
||||
<Input id="Email" class="mt-7" placeholder={m.your_email()} bind:value={email} />
|
||||
<div class="mt-8 flex justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={'/login/alternative' + page.url.search}
|
||||
>Go back</Button
|
||||
>{m.go_back()}</Button
|
||||
>
|
||||
<Button class="w-full" type="submit" {isLoading}>Submit</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>{m.submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store.js';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
@@ -33,18 +34,16 @@
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">
|
||||
{`${$appConfigStore.appName} Setup`}
|
||||
{m.appname_setup({ appName: $appConfigStore.appName })}
|
||||
</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
You're about to sign in to the initial admin account. Anyone with this link can access the
|
||||
account until a passkey is added. Please set up a passkey as soon as possible to prevent
|
||||
unauthorized access.
|
||||
{m.you_are_about_to_sign_in_to_the_initial_admin_account()}
|
||||
</p>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>{m.continue()}</Button>
|
||||
{/if}
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util.js';
|
||||
@@ -22,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Logout</title>
|
||||
<title>{m.logout()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
@@ -31,13 +32,13 @@
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">Sign out</h1>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.sign_out()}</h1>
|
||||
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Do you want to sign out of Pocket ID with the account <b>{$userStore?.username}</b>?
|
||||
{@html m.do_you_want_to_sign_out_of_pocketid_with_the_account({ username: $userStore?.username ?? '' })}
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
<Button class="w-full" variant="secondary" onclick={() => history.back()}>Cancel</Button>
|
||||
<Button class="w-full" {isLoading} onclick={signOut}>Sign out</Button>
|
||||
<Button class="w-full" variant="secondary" onclick={() => history.back()}>{m.cancel()}</Button>
|
||||
<Button class="w-full" {isLoading} onclick={signOut}>{m.sign_out()}</Button>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { LucideExternalLink } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
children,
|
||||
@@ -16,19 +17,19 @@
|
||||
const { versionInformation } = data;
|
||||
|
||||
let links = $state([
|
||||
{ href: '/settings/account', label: 'My Account' },
|
||||
{ href: '/settings/audit-log', label: 'Audit Log' }
|
||||
{ href: '/settings/account', label: m.my_account() },
|
||||
{ href: '/settings/audit-log', label: m.audit_log() }
|
||||
]);
|
||||
|
||||
if ($userStore?.isAdmin) {
|
||||
links = [
|
||||
// svelte-ignore state_referenced_locally
|
||||
...links,
|
||||
{ href: '/settings/admin/users', label: 'Users' },
|
||||
{ href: '/settings/admin/user-groups', label: 'User Groups' },
|
||||
{ href: '/settings/admin/oidc-clients', label: 'OIDC Clients' },
|
||||
{ href: '/settings/admin/api-keys', label: 'API Keys' },
|
||||
{ href: '/settings/admin/application-configuration', label: 'Application Configuration' }
|
||||
{ href: '/settings/admin/users', label: m.users() },
|
||||
{ href: '/settings/admin/user-groups', label: m.user_groups() },
|
||||
{ href: '/settings/admin/oidc-clients', label: m.oidc_clients() },
|
||||
{ href: '/settings/admin/api-keys', label: m.api_keys() },
|
||||
{ href: '/settings/admin/application-configuration', label: m.application_configuration() }
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -40,7 +41,7 @@
|
||||
>
|
||||
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||
<div class="mx-auto grid w-full gap-2">
|
||||
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||
<h1 class="mb-5 text-3xl font-semibold">{m.settings()}</h1>
|
||||
</div>
|
||||
<nav class="text-muted-foreground grid gap-4 text-sm">
|
||||
{#each links as { href, label }}
|
||||
@@ -54,7 +55,7 @@
|
||||
target="_blank"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
Update Pocket ID <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
||||
{m.update_pocket_id()} <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
@@ -65,7 +66,7 @@
|
||||
</main>
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-muted-foreground py-3 text-xs">
|
||||
Powered by <a
|
||||
{m.powered_by()} <a
|
||||
class="text-foreground"
|
||||
href="https://github.com/pocket-id/pocket-id"
|
||||
target="_blank">Pocket ID</a
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -13,6 +14,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import ProfilePictureSettings from '../../../lib/components/form/profile-picture-settings.svelte';
|
||||
import AccountForm from './account-form.svelte';
|
||||
import LocalePicker from './locale-picker.svelte';
|
||||
import LoginCodeModal from './login-code-modal.svelte';
|
||||
import PasskeyList from './passkey-list.svelte';
|
||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||
@@ -39,7 +41,7 @@
|
||||
let success = true;
|
||||
await userService
|
||||
.updateCurrent(user)
|
||||
.then(() => toast.success('Account details updated successfully'))
|
||||
.then(() => toast.success(m.account_details_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
success = false;
|
||||
@@ -51,9 +53,7 @@
|
||||
async function updateProfilePicture(image: File) {
|
||||
await userService
|
||||
.updateCurrentUsersProfilePicture(image)
|
||||
.then(() =>
|
||||
toast.success('Profile picture updated successfully. It may take a few minutes to update.')
|
||||
)
|
||||
.then(() => toast.success(m.profile_picture_updated_successfully()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
@@ -72,24 +72,22 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Account Settings</title>
|
||||
<title>{m.account_settings()}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if passkeys.length == 0}
|
||||
<Alert.Root variant="warning">
|
||||
<LucideAlertTriangle class="size-4" />
|
||||
<Alert.Title>Passkey missing</Alert.Title>
|
||||
<Alert.Title>{m.passkey_missing()}</Alert.Title>
|
||||
<Alert.Description
|
||||
>Please add a passkey to prevent losing access to your account.</Alert.Description
|
||||
>{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()}</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{:else if passkeys.length == 1}
|
||||
<Alert.Root variant="warning" dismissibleId="single-passkey">
|
||||
<LucideAlertTriangle class="size-4" />
|
||||
<Alert.Title>Single Passkey Configured</Alert.Title>
|
||||
<Alert.Description
|
||||
>It is recommended to add more than one passkey to avoid losing access to your account.</Alert.Description
|
||||
>
|
||||
<Alert.Title>{m.single_passkey_configured()}</Alert.Title>
|
||||
<Alert.Description>{m.it_is_recommended_to_add_more_than_one_passkey()}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
|
||||
@@ -99,7 +97,7 @@
|
||||
>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Account Details</Card.Title>
|
||||
<Card.Title>{m.account_details()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AccountForm {account} callback={updateAccount} />
|
||||
@@ -122,12 +120,12 @@
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Passkeys</Card.Title>
|
||||
<Card.Title>{m.passkeys()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
Manage your passkeys that you can use to authenticate yourself.
|
||||
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button size="sm" class="ml-3" on:click={createPasskey}>Add Passkey</Button>
|
||||
<Button size="sm" class="ml-3" on:click={createPasskey}>{m.add_passkey()}</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if passkeys.length != 0}
|
||||
@@ -141,12 +139,28 @@
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Login Code</Card.Title>
|
||||
<Card.Title>{m.login_code()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
Create a one-time login code to sign in from a different device without a passkey.
|
||||
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}>Create</Button>
|
||||
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}
|
||||
>{m.create()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>{m.language()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
{m.select_the_language_you_want_to_use()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<LocalePicker />
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { UserCreate } from '$lib/types/user.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
@@ -24,7 +25,7 @@
|
||||
.max(30)
|
||||
.regex(
|
||||
/^[a-z0-9_@.-]+$/,
|
||||
"Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols"
|
||||
m.username_can_only_contain()
|
||||
),
|
||||
email: z.string().email(),
|
||||
isAdmin: z.boolean()
|
||||
@@ -36,7 +37,7 @@
|
||||
const data = form.validate();
|
||||
if (!data) return;
|
||||
isLoading = true;
|
||||
const success = await callback(data);
|
||||
await callback(data);
|
||||
// Reset form if user was successfully created
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -45,21 +46,21 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label="First name" bind:input={$inputs.firstName} />
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label="Last name" bind:input={$inputs.lastName} />
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label="Email" bind:input={$inputs.email} />
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label="Username" bind:input={$inputs.username} />
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
39
frontend/src/routes/settings/account/locale-picker.svelte
Normal file
39
frontend/src/routes/settings/account/locale-picker.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { getLocale, setLocale, type Locale } from '$lib/paraglide/runtime';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
|
||||
const userService = new UserService();
|
||||
const currentLocale = getLocale();
|
||||
|
||||
const locales = {
|
||||
en: 'English',
|
||||
nl: 'Nederlands'
|
||||
};
|
||||
|
||||
function updateLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
userService.updateCurrent({
|
||||
...$userStore!,
|
||||
locale
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: locales[currentLocale],
|
||||
value: currentLocale
|
||||
}}
|
||||
onSelectedChange={(v) => updateLocale(v!.value)}
|
||||
>
|
||||
<Select.Trigger class="h-9 max-w-[200px]" aria-label="Select locale">
|
||||
<Select.Value>{locales[currentLocale]}</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.entries(locales) as [value, label]}
|
||||
<Select.Item {value}>{label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -3,6 +3,7 @@
|
||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
|
||||
@@ -37,9 +38,9 @@
|
||||
<Dialog.Root open={!!code} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Login Code</Dialog.Title>
|
||||
<Dialog.Title>{m.login_code()}</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Sign in using the following code. The code will expire in 15 minutes.
|
||||
>{m.sign_in_using_the_following_code_the_code_will_expire_in_minutes()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
@@ -49,7 +50,7 @@
|
||||
</CopyToClipboard>
|
||||
<div class="text-muted-foreground flex items-center justify-center gap-3">
|
||||
<Separator />
|
||||
<p class="text-nowrap text-xs">or visit</p>
|
||||
<p class="text-nowrap text-xs">{m.or_visit()}</p>
|
||||
<Separator />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
|
||||
|
||||
@@ -17,16 +18,16 @@
|
||||
|
||||
async function deletePasskey(passkey: Passkey) {
|
||||
openConfirmDialog({
|
||||
title: `Delete ${passkey.name}`,
|
||||
message: 'Are you sure you want to delete this passkey?',
|
||||
title: m.delete_passkey_name({ passkeyName: passkey.name }),
|
||||
message: m.are_you_sure_you_want_to_delete_this_passkey(),
|
||||
confirm: {
|
||||
label: 'Delete',
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await webauthnService.removeCredential(passkey.id);
|
||||
passkeys = await webauthnService.listCredentials();
|
||||
toast.success('Passkey deleted successfully');
|
||||
toast.success(m.passkey_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -44,7 +45,7 @@
|
||||
<div>
|
||||
<p>{passkey.name}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Added on {new Date(passkey.createdAt).toLocaleDateString()}
|
||||
{m.added_on()} {new Date(passkey.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,13 +54,13 @@
|
||||
on:click={() => (passkeyToRename = passkey)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Rename"><LucidePencil class="h-3 w-3" /></Button
|
||||
aria-label={m.rename()}><LucidePencil class="h-3 w-3" /></Button
|
||||
>
|
||||
<Button
|
||||
on:click={() => deletePasskey(passkey)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Delete"><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
||||
aria-label={m.delete()}><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -35,7 +36,7 @@
|
||||
.updateCredentialName(passkey!.id, name)
|
||||
.then(() => {
|
||||
passkey = null;
|
||||
toast.success('Passkey name updated successfully');
|
||||
toast.success(m.passkey_name_updated_successfully());
|
||||
callback?.();
|
||||
})
|
||||
.catch(axiosErrorToast);
|
||||
@@ -45,16 +46,16 @@
|
||||
<Dialog.Root open={!!passkey} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Name Passkey</Dialog.Title>
|
||||
<Dialog.Description>Name your passkey to easily identify it later.</Dialog.Description>
|
||||
<Dialog.Title>{m.name_passkey()}</Dialog.Title>
|
||||
<Dialog.Description>{m.name_your_passkey_to_easily_identify_it_later()}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid items-center gap-4 sm:grid-cols-4">
|
||||
<Label for="name" class="sm:text-right">Name</Label>
|
||||
<Label for="name" class="sm:text-right">{m.name()}</Label>
|
||||
<Input id="name" bind:value={name} class="col-span-3" />
|
||||
</div>
|
||||
<Dialog.Footer class="mt-4">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">{m.save()}</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import ApiKeyDialog from './api-key-dialog.svelte';
|
||||
import ApiKeyForm from './api-key-form.svelte';
|
||||
import ApiKeyList from './api-key-list.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let apiKeys = $state(data.apiKeys);
|
||||
@@ -35,18 +36,18 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>API Keys</title>
|
||||
<title>{m.api_keys()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create API Key</Card.Title>
|
||||
<Card.Description>Add a new API key for programmatic access.</Card.Description>
|
||||
<Card.Title>{m.create_api_key()}</Card.Title>
|
||||
<Card.Description>{m.add_a_new_api_key_for_programmatic_access()}</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button on:click={() => (expandAddApiKey = true)}>Add API Key</Button>
|
||||
<Button on:click={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="h-5 w-5" />
|
||||
@@ -65,7 +66,7 @@
|
||||
|
||||
<Card.Root class="mt-6">
|
||||
<Card.Header>
|
||||
<Card.Title>Manage API Keys</Card.Title>
|
||||
<Card.Title>{m.manage_api_keys()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { ApiKeyResponse } from '$lib/types/api-key.type';
|
||||
|
||||
let {
|
||||
@@ -20,22 +21,22 @@
|
||||
<Dialog.Root open={!!apiKeyResponse} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md" closeButton={false}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>API Key Created</Dialog.Title>
|
||||
<Dialog.Title>{m.api_key_created()}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
For security reasons, this key will only be shown once. Please store it securely.
|
||||
{m.for_security_reasons_this_key_will_only_be_shown_once()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
{#if apiKeyResponse}
|
||||
<div>
|
||||
<div class="mb-2 font-medium">Name</div>
|
||||
<div class="mb-2 font-medium">{m.name()}</div>
|
||||
<p class="text-muted-foreground">{apiKeyResponse.apiKey.name}</p>
|
||||
|
||||
{#if apiKeyResponse.apiKey.description}
|
||||
<div class="mb-2 mt-4 font-medium">Description</div>
|
||||
<div class="mb-2 mt-4 font-medium">{m.description()}</div>
|
||||
<p class="text-muted-foreground">{apiKeyResponse.apiKey.description}</p>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2 mt-4 font-medium">API Key</div>
|
||||
<div class="mb-2 mt-4 font-medium">{m.api_key()}</div>
|
||||
<div class="bg-muted rounded-md p-2">
|
||||
<CopyToClipboard value={apiKeyResponse.token}>
|
||||
<span class="break-all font-mono text-sm">{apiKeyResponse.token}</span>
|
||||
@@ -44,7 +45,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<Dialog.Footer class="mt-3">
|
||||
<Button variant="default" on:click={() => onOpenChange(false)}>Close</Button>
|
||||
<Button variant="default" on:click={() => onOpenChange(false)}>{m.close()}</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { ApiKeyCreate } from '$lib/types/api-key.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
@@ -26,10 +27,10 @@
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3, 'Name must be at least 3 characters')
|
||||
.max(50, 'Name cannot exceed 50 characters'),
|
||||
.min(3, m.name_must_be_at_least_3_characters())
|
||||
.max(50, m.name_cannot_exceed_50_characters()),
|
||||
description: z.string().default(''),
|
||||
expiresAt: z.date().min(new Date(), 'Expiration date must be in the future')
|
||||
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, apiKey);
|
||||
@@ -54,25 +55,25 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput
|
||||
label="Name"
|
||||
label={m.name()}
|
||||
bind:input={$inputs.name}
|
||||
description="Name to identify this API key."
|
||||
description={m.name_to_identify_this_api_key()}
|
||||
/>
|
||||
<FormInput
|
||||
label="Expires At"
|
||||
label={m.expires_at()}
|
||||
type="date"
|
||||
description="When this API key will expire."
|
||||
description={m.when_this_api_key_will_expire()}
|
||||
bind:input={$inputs.expiresAt}
|
||||
/>
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<FormInput
|
||||
label="Description"
|
||||
description="Optional description to help identify this key's purpose."
|
||||
label={m.description()}
|
||||
description={m.optional_description_to_help_identify_this_keys_purpose()}
|
||||
bind:input={$inputs.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type { ApiKey } from '$lib/types/api-key.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
@@ -21,22 +22,22 @@
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return 'Never';
|
||||
if (!dateStr) return m.never();
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
function revokeApiKey(apiKey: ApiKey) {
|
||||
openConfirmDialog({
|
||||
title: 'Revoke API Key',
|
||||
message: `Are you sure you want to revoke the API key "${apiKey.name}"? This will break any integrations using this key.`,
|
||||
title: m.revoke_api_key(),
|
||||
message: m.are_you_sure_you_want_to_revoke_the_api_key_apikeyname({ apiKeyName: apiKey.name }),
|
||||
confirm: {
|
||||
label: 'Revoke',
|
||||
label: m.revoke(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await apiKeyService.revoke(apiKey.id);
|
||||
apiKeys = await apiKeyService.list(requestOptions);
|
||||
toast.success('API key revoked successfully');
|
||||
toast.success(m.api_key_revoked_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -52,11 +53,11 @@
|
||||
onRefresh={async (o) => (apiKeys = await apiKeyService.list(o))}
|
||||
withoutSearch
|
||||
columns={[
|
||||
{ label: 'Name', sortColumn: 'name' },
|
||||
{ label: 'Description' },
|
||||
{ label: 'Expires At', sortColumn: 'expiresAt' },
|
||||
{ label: 'Last Used', sortColumn: 'lastUsedAt' },
|
||||
{ label: 'Actions', hidden: true }
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.description() },
|
||||
{ label: m.expires_at(), sortColumn: 'expiresAt' },
|
||||
{ label: m.last_used(), sortColumn: 'lastUsedAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -65,7 +66,7 @@
|
||||
<Table.Cell>{formatDate(item.expiresAt)}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.lastUsedAt)}</Table.Cell>
|
||||
<Table.Cell class="flex justify-end">
|
||||
<Button on:click={() => revokeApiKey(item)} size="sm" variant="outline" aria-label="Revoke"
|
||||
<Button on:click={() => revokeApiKey(item)} size="sm" variant="outline" aria-label={m.revoke()}
|
||||
><LucideBan class="h-3 w-3 text-red-500" /></Button
|
||||
>
|
||||
</Table.Cell>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
||||
import UpdateApplicationImages from './update-application-images.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let appConfig = $state(data.appConfig);
|
||||
@@ -46,36 +47,35 @@
|
||||
: Promise.resolve();
|
||||
|
||||
await Promise.all([lightLogoPromise, darkLogoPromise, backgroundImagePromise, faviconPromise])
|
||||
.then(() => toast.success('Images updated successfully'))
|
||||
.then(() => toast.success(m.images_updated_successfully()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Application Configuration</title>
|
||||
<title>{m.application_configuration()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<CollapsibleCard id="application-configuration-general" title="General" defaultExpanded>
|
||||
<CollapsibleCard id="application-configuration-general" title={m.general()} defaultExpanded>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
title="Email"
|
||||
description="Enable email notifications to alert users when a login is detected from a new device or
|
||||
location."
|
||||
title={m.email()}
|
||||
description={m.enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location()}
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
title="LDAP"
|
||||
description="Configure LDAP settings to sync users and groups from an LDAP server."
|
||||
title={m.ldap()}
|
||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard id="application-configuration-images" title="Images">
|
||||
<CollapsibleCard id="application-configuration-images" title={m.images()}>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FileInput from '$lib/components/form/file-input.svelte';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
<span
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
Update
|
||||
{m.update()}
|
||||
</span>
|
||||
</div>
|
||||
</FileInput>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
@@ -55,7 +56,7 @@
|
||||
appConfig[key] = value;
|
||||
});
|
||||
|
||||
toast.success('Email configuration updated successfully');
|
||||
toast.success(m.email_configuration_updated_successfully());
|
||||
return true;
|
||||
}
|
||||
async function onTestEmail() {
|
||||
@@ -64,11 +65,11 @@
|
||||
|
||||
if (hasChanges) {
|
||||
openConfirmDialog({
|
||||
title: 'Save changes?',
|
||||
title: m.save_changes_question(),
|
||||
message:
|
||||
'You have to save the changes before sending a test email. Do you want to save now?',
|
||||
m.you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now(),
|
||||
confirm: {
|
||||
label: 'Save and send',
|
||||
label: m.save_and_send(),
|
||||
action: async () => {
|
||||
const saved = await onSubmit();
|
||||
if (saved) {
|
||||
@@ -86,9 +87,9 @@
|
||||
isSendingTestEmail = true;
|
||||
await appConfigService
|
||||
.sendTestEmail()
|
||||
.then(() => toast.success('Test email sent successfully to your email address.'))
|
||||
.then(() => toast.success(m.test_email_sent_successfully()))
|
||||
.catch(() =>
|
||||
toast.error('Failed to send test email. Check the server logs for more information.')
|
||||
toast.error(m.failed_to_send_test_email())
|
||||
)
|
||||
.finally(() => (isSendingTestEmail = false));
|
||||
}
|
||||
@@ -96,21 +97,21 @@
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<fieldset disabled={uiConfigDisabled}>
|
||||
<h4 class="text-lg font-semibold">SMTP Configuration</h4>
|
||||
<h4 class="text-lg font-semibold">{m.smtp_configuration()}</h4>
|
||||
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
|
||||
<FormInput label="SMTP Port" type="number" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
||||
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
|
||||
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
|
||||
<FormInput label={m.smtp_host()} bind:input={$inputs.smtpHost} />
|
||||
<FormInput label={m.smtp_port()} type="number" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label={m.smtp_user()} bind:input={$inputs.smtpUser} />
|
||||
<FormInput label={m.smtp_password()} type="password" bind:input={$inputs.smtpPassword} />
|
||||
<FormInput label={m.smtp_from()} bind:input={$inputs.smtpFrom} />
|
||||
<div class="grid gap-2">
|
||||
<Label class="mb-0" for="smtp-tls">SMTP TLS Option</Label>
|
||||
<Label class="mb-0" for="smtp-tls">{m.smtp_tls_option()}</Label>
|
||||
<Select.Root
|
||||
selected={{ value: $inputs.smtpTls.value, label: tlsOptions[$inputs.smtpTls.value] }}
|
||||
onSelectedChange={(v) => ($inputs.smtpTls.value = v!.value)}
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Email TLS Option" />
|
||||
<Select.Value placeholder={m.email_tls_option()} />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="none" label="None" />
|
||||
@@ -121,31 +122,31 @@
|
||||
</div>
|
||||
<CheckboxWithLabel
|
||||
id="skip-cert-verify"
|
||||
label="Skip Certificate Verification"
|
||||
description="This can be useful for self-signed certificates."
|
||||
label={m.skip_certificate_verification()}
|
||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||
bind:checked={$inputs.smtpSkipCertVerify.value}
|
||||
/>
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">Enabled Emails</h4>
|
||||
<h4 class="mt-10 text-lg font-semibold">{m.enabled_emails()}</h4>
|
||||
<div class="mt-4 flex flex-col gap-5">
|
||||
<CheckboxWithLabel
|
||||
id="email-login-notification"
|
||||
label="Email Login Notification"
|
||||
description="Send an email to the user when they log in from a new device."
|
||||
label={m.email_login_notification()}
|
||||
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
|
||||
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="email-login"
|
||||
label="Email Login"
|
||||
description="Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry."
|
||||
label={m.email_login()}
|
||||
description={m.allow_users_to_sign_in_with_a_login_code_sent_to_their_email()}
|
||||
bind:checked={$inputs.emailOneTimeAccessEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
|
||||
>Send test email</Button
|
||||
>{m.send_test_email()}</Button
|
||||
>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>Save</Button>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -39,35 +40,35 @@
|
||||
if (!data) return;
|
||||
isLoading = true;
|
||||
await callback(data).finally(() => (isLoading = false));
|
||||
toast.success('Application configuration updated successfully');
|
||||
toast.success(m.application_configuration_updated_successfully());
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<fieldset class="flex flex-col gap-5" disabled={uiConfigDisabled}>
|
||||
<div class="flex flex-col gap-5">
|
||||
<FormInput label="Application Name" bind:input={$inputs.appName} />
|
||||
<FormInput label={m.application_name()} bind:input={$inputs.appName} />
|
||||
<FormInput
|
||||
label="Session Duration"
|
||||
label={m.session_duration()}
|
||||
type="number"
|
||||
description="The duration of a session in minutes before the user has to sign in again."
|
||||
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
|
||||
bind:input={$inputs.sessionDuration}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="self-account-editing"
|
||||
label="Enable Self-Account Editing"
|
||||
description="Whether the users should be able to edit their own account details."
|
||||
label={m.enable_self_account_editing()}
|
||||
description={m.whether_the_users_should_be_able_to_edit_their_own_account_details()}
|
||||
bind:checked={$inputs.allowOwnAccountEdit.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="emails-verified"
|
||||
label="Emails Verified"
|
||||
description="Whether the user's email should be marked as verified for the OIDC clients."
|
||||
label={m.emails_verified()}
|
||||
description={m.whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients()}
|
||||
bind:checked={$inputs.emailsVerified.value}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -74,14 +75,14 @@
|
||||
...data,
|
||||
ldapEnabled: true
|
||||
});
|
||||
toast.success('LDAP configuration updated successfully');
|
||||
toast.success(m.ldap_configuration_updated_successfully());
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onDisable() {
|
||||
ldapEnabled = false;
|
||||
await callback({ ldapEnabled });
|
||||
toast.success('LDAP disabled successfully');
|
||||
toast.success(m.ldap_disabled_successfully());
|
||||
}
|
||||
|
||||
async function onEnable() {
|
||||
@@ -94,7 +95,7 @@
|
||||
ldapSyncing = true;
|
||||
await appConfigService
|
||||
.syncLdap()
|
||||
.then(() => toast.success('LDAP sync finished'))
|
||||
.then(() => toast.success(m.ldap_sync_finished()))
|
||||
.catch(axiosErrorToast);
|
||||
|
||||
ldapSyncing = false;
|
||||
@@ -102,98 +103,98 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<h4 class="text-lg font-semibold">Client Configuration</h4>
|
||||
<h4 class="text-lg font-semibold">{m.client_configuration()}</h4>
|
||||
<fieldset disabled={uiConfigDisabled}>
|
||||
<div class="mt-4 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput
|
||||
label="LDAP URL"
|
||||
label={m.ldap_url()}
|
||||
placeholder="ldap://example.com:389"
|
||||
bind:input={$inputs.ldapUrl}
|
||||
/>
|
||||
<FormInput
|
||||
label="LDAP Bind DN"
|
||||
label={m.ldap_bind_dn()}
|
||||
placeholder="cn=people,dc=example,dc=com"
|
||||
bind:input={$inputs.ldapBindDn}
|
||||
/>
|
||||
<FormInput label="LDAP Bind Password" type="password" bind:input={$inputs.ldapBindPassword} />
|
||||
<FormInput label={m.ldap_bind_password()} type="password" bind:input={$inputs.ldapBindPassword} />
|
||||
<FormInput
|
||||
label="LDAP Base DN"
|
||||
label={m.ldap_base_dn()}
|
||||
placeholder="dc=example,dc=com"
|
||||
bind:input={$inputs.ldapBase}
|
||||
/>
|
||||
<FormInput
|
||||
label="User Search Filter"
|
||||
description="The Search filter to use to search/sync users."
|
||||
label={m.user_search_filter()}
|
||||
description={m.the_search_filter_to_use_to_search_or_sync_users()}
|
||||
placeholder="(objectClass=person)"
|
||||
bind:input={$inputs.ldapUserSearchFilter}
|
||||
/>
|
||||
<FormInput
|
||||
label="Groups Search Filter"
|
||||
description="The Search filter to use to search/sync groups."
|
||||
label={m.groups_search_filter()}
|
||||
description={m.the_search_filter_to_use_to_search_or_sync_groups()}
|
||||
placeholder="(objectClass=groupOfNames)"
|
||||
bind:input={$inputs.ldapUserGroupSearchFilter}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="skip-cert-verify"
|
||||
label="Skip Certificate Verification"
|
||||
description="This can be useful for self-signed certificates."
|
||||
label={m.skip_certificate_verification()}
|
||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||
bind:checked={$inputs.ldapSkipCertVerify.value}
|
||||
/>
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">Attribute Mapping</h4>
|
||||
<h4 class="mt-10 text-lg font-semibold">{m.attribute_mapping()}</h4>
|
||||
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||
<FormInput
|
||||
label="User Unique Identifier Attribute"
|
||||
description="The value of this attribute should never change."
|
||||
label={m.user_unique_identifier_attribute()}
|
||||
description={m.the_value_of_this_attribute_should_never_change()}
|
||||
placeholder="uuid"
|
||||
bind:input={$inputs.ldapAttributeUserUniqueIdentifier}
|
||||
/>
|
||||
<FormInput
|
||||
label="Username Attribute"
|
||||
label={m.username_attribute()}
|
||||
placeholder="uid"
|
||||
bind:input={$inputs.ldapAttributeUserUsername}
|
||||
/>
|
||||
<FormInput
|
||||
label="User Mail Attribute"
|
||||
label={m.user_mail_attribute()}
|
||||
placeholder="mail"
|
||||
bind:input={$inputs.ldapAttributeUserEmail}
|
||||
/>
|
||||
<FormInput
|
||||
label="User First Name Attribute"
|
||||
label={m.user_first_name_attribute()}
|
||||
placeholder="givenName"
|
||||
bind:input={$inputs.ldapAttributeUserFirstName}
|
||||
/>
|
||||
<FormInput
|
||||
label="User Last Name Attribute"
|
||||
label={m.user_last_name_attribute()}
|
||||
placeholder="sn"
|
||||
bind:input={$inputs.ldapAttributeUserLastName}
|
||||
/>
|
||||
<FormInput
|
||||
label="User Profile Picture Attribute"
|
||||
description="The value of this attribute can either be a URL, a binary or a base64 encoded image."
|
||||
label={m.user_profile_picture_attribute()}
|
||||
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
||||
placeholder="jpegPhoto"
|
||||
bind:input={$inputs.ldapAttributeUserProfilePicture}
|
||||
/>
|
||||
<FormInput
|
||||
label="Group Members Attribute"
|
||||
description="The attribute to use for querying members of a group."
|
||||
label={m.group_members_attribute()}
|
||||
description={m.the_attribute_to_use_for_querying_members_of_a_group()}
|
||||
placeholder="member"
|
||||
bind:input={$inputs.ldapAttributeGroupMember}
|
||||
/>
|
||||
<FormInput
|
||||
label="Group Unique Identifier Attribute"
|
||||
description="The value of this attribute should never change."
|
||||
label={m.group_unique_identifier_attribute()}
|
||||
description={m.the_value_of_this_attribute_should_never_change()}
|
||||
placeholder="uuid"
|
||||
bind:input={$inputs.ldapAttributeGroupUniqueIdentifier}
|
||||
/>
|
||||
<FormInput
|
||||
label="Group Name Attribute"
|
||||
label={m.group_name_attribute()}
|
||||
placeholder="cn"
|
||||
bind:input={$inputs.ldapAttributeGroupName}
|
||||
/>
|
||||
<FormInput
|
||||
label="Admin Group Name"
|
||||
description="Members of this group will have Admin Privileges in Pocket ID."
|
||||
label={m.admin_group_name()}
|
||||
description={m.members_of_this_group_will_have_admin_privileges_in_pocketid()}
|
||||
placeholder="_admin_group_name"
|
||||
bind:input={$inputs.ldapAttributeAdminGroup}
|
||||
/>
|
||||
@@ -202,11 +203,11 @@
|
||||
|
||||
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||
{#if ldapEnabled}
|
||||
<Button variant="secondary" onclick={onDisable} disabled={uiConfigDisabled}>Disable</Button>
|
||||
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>Sync now</Button>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>Save</Button>
|
||||
<Button variant="secondary" onclick={onDisable} disabled={uiConfigDisabled}>{m.disable()}</Button>
|
||||
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>{m.sync_now()}</Button>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button>
|
||||
{:else}
|
||||
<Button onclick={onEnable} disabled={uiConfigDisabled}>Enable</Button>
|
||||
<Button onclick={onEnable} disabled={uiConfigDisabled}>{m.enable()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import ApplicationImage from './application-image.svelte';
|
||||
|
||||
let {
|
||||
@@ -23,7 +24,7 @@
|
||||
<ApplicationImage
|
||||
id="favicon"
|
||||
imageClass="h-14 w-14 p-2"
|
||||
label="Favicon"
|
||||
label={m.favicon()}
|
||||
bind:image={favicon}
|
||||
imageURL="/api/application-configuration/favicon"
|
||||
accept="image/x-icon"
|
||||
@@ -31,7 +32,7 @@
|
||||
<ApplicationImage
|
||||
id="logo-light"
|
||||
imageClass="h-32 w-32"
|
||||
label="Light Mode Logo"
|
||||
label={m.light_mode_logo()}
|
||||
bind:image={logoLight}
|
||||
imageURL="/api/application-configuration/logo?light=true"
|
||||
forceColorScheme="light"
|
||||
@@ -39,7 +40,7 @@
|
||||
<ApplicationImage
|
||||
id="logo-dark"
|
||||
imageClass="h-32 w-32"
|
||||
label="Dark Mode Logo"
|
||||
label={m.dark_mode_logo()}
|
||||
bind:image={logoDark}
|
||||
imageURL="/api/application-configuration/logo?light=false"
|
||||
forceColorScheme="dark"
|
||||
@@ -47,13 +48,13 @@
|
||||
<ApplicationImage
|
||||
id="background-image"
|
||||
imageClass="h-[350px] max-w-[500px]"
|
||||
label="Background Image"
|
||||
label={m.background_image()}
|
||||
bind:image={backgroundImage}
|
||||
imageURL="/api/application-configuration/background-image"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button class="mt-5" onclick={() => callback(logoLight, logoDark, backgroundImage, favicon)}
|
||||
>Save</Button
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import OIDCClientForm from './oidc-client-form.svelte';
|
||||
import OIDCClientList from './oidc-client-list.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let clients = $state(data.clients);
|
||||
@@ -29,7 +30,7 @@
|
||||
const clientSecret = await oidcService.createClientSecret(createdClient.id);
|
||||
clientSecretStore.set(clientSecret);
|
||||
goto(`/settings/admin/oidc-clients/${createdClient.id}`);
|
||||
toast.success('OIDC client created successfully');
|
||||
toast.success(m.oidc_client_created_successfully());
|
||||
return true;
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -39,18 +40,18 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>OIDC Clients</title>
|
||||
<title>{m.oidc_clients()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create OIDC Client</Card.Title>
|
||||
<Card.Description>Add a new OIDC client to {$appConfigStore.appName}.</Card.Description>
|
||||
<Card.Title>{m.create_oidc_client()}</Card.Title>
|
||||
<Card.Description>{m.add_a_new_oidc_client_to_appname({ appName: $appConfigStore.appName})}</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddClient}
|
||||
<Button on:click={() => (expandAddClient = true)}>Add OIDC Client</Button>
|
||||
<Button on:click={() => (expandAddClient = true)}>{m.add_oidc_client()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddClient = false)}>
|
||||
<LucideMinus class="h-5 w-5" />
|
||||
@@ -69,7 +70,7 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Manage OIDC Clients</Card.Title>
|
||||
<Card.Title>{m.manage_oidc_clients()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<OIDCClientList {clients} requestOptions={clientsRequestOptions} />
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import OidcForm from '../oidc-client-form.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let client = $state({
|
||||
@@ -27,13 +28,13 @@
|
||||
const oidcService = new OidcService();
|
||||
|
||||
const setupDetails = $state({
|
||||
'Authorization URL': `https://${$page.url.hostname}/authorize`,
|
||||
'OIDC Discovery URL': `https://${$page.url.hostname}/.well-known/openid-configuration`,
|
||||
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
||||
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||
'Logout URL': `https://${$page.url.hostname}/api/oidc/end-session`,
|
||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||
PKCE: client.pkceEnabled ? 'Enabled' : 'Disabled'
|
||||
[m.authorization_url()]: `https://${$page.url.hostname}/authorize`,
|
||||
[m.oidc_discovery_url()]: `https://${$page.url.hostname}/.well-known/openid-configuration`,
|
||||
[m.token_url()]: `https://${$page.url.hostname}/api/oidc/token`,
|
||||
[m.userinfo_url()]: `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||
[m.logout_url()]: `https://${$page.url.hostname}/api/oidc/end-session`,
|
||||
[m.certificate_url()]: `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled()
|
||||
});
|
||||
|
||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||
@@ -45,11 +46,11 @@
|
||||
: Promise.resolve();
|
||||
|
||||
client.isPublic = updatedClient.isPublic;
|
||||
setupDetails.PKCE = updatedClient.pkceEnabled ? 'Enabled' : 'Disabled';
|
||||
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
|
||||
|
||||
await Promise.all([dataPromise, imagePromise])
|
||||
.then(() => {
|
||||
toast.success('OIDC client updated successfully');
|
||||
toast.success(m.oidc_client_updated_successfully());
|
||||
})
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
@@ -61,17 +62,17 @@
|
||||
|
||||
async function createClientSecret() {
|
||||
openConfirmDialog({
|
||||
title: 'Create new client secret',
|
||||
title: m.create_new_client_secret(),
|
||||
message:
|
||||
'Are you sure you want to create a new client secret? The old one will be invalidated.',
|
||||
m.are_you_sure_you_want_to_create_a_new_client_secret(),
|
||||
confirm: {
|
||||
label: 'Generate',
|
||||
label: m.generate(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
const clientSecret = await oidcService.createClientSecret(client.id);
|
||||
clientSecretStore.set(clientSecret);
|
||||
toast.success('New client secret created successfully');
|
||||
toast.success(m.new_client_secret_created_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -84,7 +85,7 @@
|
||||
await oidcService
|
||||
.updateAllowedUserGroups(client.id, allowedGroups)
|
||||
.then(() => {
|
||||
toast.success('Allowed user groups updated successfully');
|
||||
toast.success(m.allowed_user_groups_updated_successfully());
|
||||
})
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
@@ -97,12 +98,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>OIDC Client {client.name}</title>
|
||||
<title>{m.oidc_client_name({ name: client.name })}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/oidc-clients"
|
||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||
><LucideChevronLeft class="h-5 w-5" /> {m.back()}</a
|
||||
>
|
||||
</div>
|
||||
<Card.Root>
|
||||
@@ -112,14 +113,14 @@
|
||||
<Card.Content>
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Label class="mb-0 w-44">Client ID</Label>
|
||||
<Label class="mb-0 w-44">{m.client_id()}</Label>
|
||||
<CopyToClipboard value={client.id}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{#if !client.isPublic}
|
||||
<div class="mb-2 mt-1 flex flex-col sm:flex-row sm:items-center">
|
||||
<Label class="mb-0 w-44">Client secret</Label>
|
||||
<Label class="mb-0 w-44">{m.client_secret()}</Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
@@ -158,7 +159,7 @@
|
||||
{#if !showAllDetails}
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Button on:click={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||
>Show more details</Button
|
||||
>{m.show_more_details()}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -172,11 +173,11 @@
|
||||
</Card.Root>
|
||||
<CollapsibleCard
|
||||
id="allowed-user-groups"
|
||||
title="Allowed User Groups"
|
||||
description="Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client."
|
||||
title={m.allowed_user_groups()}
|
||||
description={m.add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups()}
|
||||
>
|
||||
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button on:click={() => updateUserGroupClients(client.allowedUserGroupIds)}>Save</Button>
|
||||
<Button on:click={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
oneTimeLink = $bindable()
|
||||
@@ -19,13 +20,12 @@
|
||||
<Dialog.Root open={!!oneTimeLink} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>One Time Link</Dialog.Title>
|
||||
<Dialog.Title>{m.one_time_link()}</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Use this link to sign in once. This is needed for users who haven't added a passkey yet or
|
||||
have lost it.</Dialog.Description
|
||||
>{m.use_this_link_to_sign_in_once()}</Dialog.Description
|
||||
>
|
||||
</Dialog.Header>
|
||||
<Label for="one-time-link">One Time Link</Label>
|
||||
<Label for="one-time-link">{m.one_time_link()}</Label>
|
||||
<Input id="one-time-link" value={oneTimeLink} readonly />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideMinus, LucidePlus } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
@@ -53,7 +54,7 @@
|
||||
on:click={() => (callbackURLs = [...callbackURLs, ''])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
{callbackURLs.length === 0 ? 'Add' : 'Add another'}
|
||||
{callbackURLs.length === 0 ? m.add() : m.add_another()}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
callback,
|
||||
@@ -79,16 +80,16 @@
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-7 sm:flex-row">
|
||||
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
|
||||
<FormInput label={m.name()} class="w-full" bind:input={$inputs.name} />
|
||||
<div></div>
|
||||
<OidcCallbackUrlInput
|
||||
label="Callback URLs"
|
||||
label={m.callback_urls()}
|
||||
class="w-full"
|
||||
bind:callbackURLs={$inputs.callbackURLs.value}
|
||||
bind:error={$inputs.callbackURLs.error}
|
||||
/>
|
||||
<OidcCallbackUrlInput
|
||||
label="Logout Callback URLs"
|
||||
label={m.logout_callback_urls()}
|
||||
class="w-full"
|
||||
allowEmpty
|
||||
bind:callbackURLs={$inputs.logoutCallbackURLs.value}
|
||||
@@ -96,8 +97,8 @@
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="public-client"
|
||||
label="Public Client"
|
||||
description="Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app."
|
||||
label={m.public_client()}
|
||||
description={m.public_clients_do_not_have_a_client_secret_and_use_pkce_instead()}
|
||||
onCheckedChange={(v) => {
|
||||
if (v == true) form.setValue('pkceEnabled', true);
|
||||
}}
|
||||
@@ -105,21 +106,21 @@
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="pkce"
|
||||
label="PKCE"
|
||||
description="Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks."
|
||||
label={m.pkce()}
|
||||
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
|
||||
disabled={$inputs.isPublic.value}
|
||||
bind:checked={$inputs.pkceEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<Label for="logo">Logo</Label>
|
||||
<Label for="logo">{m.logo()}</Label>
|
||||
<div class="mt-2 flex items-end gap-3">
|
||||
{#if logoDataURL}
|
||||
<div class="bg-muted h-32 w-32 rounded-2xl p-3">
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src={logoDataURL}
|
||||
alt={`${$inputs.name.value} logo`}
|
||||
alt={m.name_logo({name: $inputs.name.value})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -131,17 +132,17 @@
|
||||
onchange={onLogoChange}
|
||||
>
|
||||
<Button variant="secondary">
|
||||
{logoDataURL ? 'Change Logo' : 'Upload Logo'}
|
||||
{logoDataURL ? m.change_logo() : m.upload_logo()}
|
||||
</Button>
|
||||
</FileInput>
|
||||
{#if logoDataURL}
|
||||
<Button variant="outline" on:click={resetLogo}>Remove Logo</Button>
|
||||
<Button variant="outline" on:click={resetLogo}>{m.remove_logo()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { LucidePencil, LucideTrash } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import OneTimeLinkModal from './client-secret.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
clients = $bindable(),
|
||||
@@ -25,16 +26,16 @@
|
||||
|
||||
async function deleteClient(client: OidcClient) {
|
||||
openConfirmDialog({
|
||||
title: `Delete ${client.name}`,
|
||||
message: 'Are you sure you want to delete this OIDC client?',
|
||||
title: m.delete_name({name: client.name}),
|
||||
message: m.are_you_sure_you_want_to_delete_this_oidc_client(),
|
||||
confirm: {
|
||||
label: 'Delete',
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await oidcService.removeClient(client.id);
|
||||
clients = await oidcService.listClients(requestOptions!);
|
||||
toast.success('OIDC client deleted successfully');
|
||||
toast.success(m.oidc_client_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -49,9 +50,9 @@
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (clients = await oidcService.listClients(o))}
|
||||
columns={[
|
||||
{ label: 'Logo' },
|
||||
{ label: 'Name', sortColumn: 'name' },
|
||||
{ label: 'Actions', hidden: true }
|
||||
{ label: m.logo() },
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -61,7 +62,7 @@
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src="/api/oidc/clients/{item.id}/logo"
|
||||
alt="{item.name} logo"
|
||||
alt={m.name_logo({name: item.name})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -72,9 +73,9 @@
|
||||
href="/settings/admin/oidc-clients/{item.id}"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Edit"><LucidePencil class="h-3 w-3 " /></Button
|
||||
aria-label={m.edit()}><LucidePencil class="h-3 w-3 " /></Button
|
||||
>
|
||||
<Button on:click={() => deleteClient(item)} size="sm" variant="outline" aria-label="Delete"
|
||||
<Button on:click={() => deleteClient(item)} size="sm" variant="outline" aria-label={m.delete()}
|
||||
><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
||||
>
|
||||
</Table.Cell>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import UserGroupForm from './user-group-form.svelte';
|
||||
import UserGroupList from './user-group-list.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let userGroups = $state(data.userGroups);
|
||||
@@ -24,7 +25,7 @@
|
||||
await userGroupService
|
||||
.create(userGroup)
|
||||
.then((createdUserGroup) => {
|
||||
toast.success('User group created successfully');
|
||||
toast.success(m.user_group_created_successfully());
|
||||
goto(`/settings/admin/user-groups/${createdUserGroup.id}`);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -36,18 +37,18 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Groups</title>
|
||||
<title>{m.user_groups()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create User Group</Card.Title>
|
||||
<Card.Description>Create a new group that can be assigned to users.</Card.Description>
|
||||
<Card.Title>{m.create_user_group()}</Card.Title>
|
||||
<Card.Description>{m.create_a_new_group_that_can_be_assigned_to_users()}</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddUserGroup}
|
||||
<Button on:click={() => (expandAddUserGroup = true)}>Add Group</Button>
|
||||
<Button on:click={() => (expandAddUserGroup = true)}>{m.add_group()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddUserGroup = false)}>
|
||||
<LucideMinus class="h-5 w-5" />
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Manage User Groups</Card.Title>
|
||||
<Card.Title>{m.manage_user_groups()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserGroupList {userGroups} requestOptions={userGroupsRequestOptions} />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import UserGroupForm from '../user-group-form.svelte';
|
||||
import UserSelection from '../user-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let userGroup = $state({
|
||||
@@ -27,7 +28,7 @@
|
||||
let success = true;
|
||||
await userGroupService
|
||||
.update(userGroup.id, updatedUserGroup)
|
||||
.then(() => toast.success('User group updated successfully'))
|
||||
.then(() => toast.success(m.user_group_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
success = false;
|
||||
@@ -39,7 +40,7 @@
|
||||
async function updateUserGroupUsers(userIds: string[]) {
|
||||
await userGroupService
|
||||
.updateUsers(userGroup.id, userIds)
|
||||
.then(() => toast.success('Users updated successfully'))
|
||||
.then(() => toast.success(m.users_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
});
|
||||
@@ -48,7 +49,7 @@
|
||||
async function updateCustomClaims() {
|
||||
await customClaimService
|
||||
.updateUserGroupCustomClaims(userGroup.id, userGroup.customClaims)
|
||||
.then(() => toast.success('Custom claims updated successfully'))
|
||||
.then(() => toast.success(m.custom_claims_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
});
|
||||
@@ -56,20 +57,20 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Group Details {userGroup.name}</title>
|
||||
<title>{m.user_group_details_name({ name: userGroup.name })}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/user-groups"
|
||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||
><LucideChevronLeft class="h-5 w-5" /> {m.back()}</a
|
||||
>
|
||||
{#if !!userGroup.ldapId}
|
||||
<Badge variant="default" class="">LDAP</Badge>
|
||||
<Badge variant="default" class="">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>General</Card.Title>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
@@ -79,8 +80,8 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Users</Card.Title>
|
||||
<Card.Description>Assign users to this group.</Card.Description>
|
||||
<Card.Title>{m.users()}</Card.Title>
|
||||
<Card.Description>{m.assign_users_to_this_group()}</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
@@ -91,7 +92,7 @@
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button
|
||||
disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||
on:click={() => updateUserGroupUsers(userGroup.userIds)}>Save</Button
|
||||
on:click={() => updateUserGroupUsers(userGroup.userIds)}>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
@@ -99,11 +100,11 @@
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-group-custom-claims"
|
||||
title="Custom Claims"
|
||||
description="Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts."
|
||||
title={m.custom_claims()}
|
||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized()}
|
||||
>
|
||||
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">Save</Button>
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
@@ -60,23 +61,23 @@
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput
|
||||
label="Friendly Name"
|
||||
description="Name that will be displayed in the UI"
|
||||
label={m.friendly_name()}
|
||||
description={m.name_that_will_be_displayed_in_the_ui()}
|
||||
bind:input={$inputs.friendlyName}
|
||||
onInput={onFriendlyNameInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput
|
||||
label="Name"
|
||||
description={`Name that will be in the "groups" claim`}
|
||||
label={m.name()}
|
||||
description={m.name_that_will_be_in_the_groups_claim()}
|
||||
bind:input={$inputs.name}
|
||||
onInput={onNameInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
@@ -26,16 +27,16 @@
|
||||
|
||||
async function deleteUserGroup(userGroup: UserGroup) {
|
||||
openConfirmDialog({
|
||||
title: `Delete ${userGroup.name}`,
|
||||
message: 'Are you sure you want to delete this user group?',
|
||||
title: m.delete_name({ name: userGroup.name }),
|
||||
message: m.are_you_sure_you_want_to_delete_this_user_group(),
|
||||
confirm: {
|
||||
label: 'Delete',
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await userGroupService.remove(userGroup.id);
|
||||
userGroups = await userGroupService.list(requestOptions!);
|
||||
toast.success('User group deleted successfully');
|
||||
toast.success(m.user_group_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -50,11 +51,11 @@
|
||||
onRefresh={async (o) => (userGroups = await userGroupService.list(o))}
|
||||
{requestOptions}
|
||||
columns={[
|
||||
{ label: 'Friendly Name', sortColumn: 'friendlyName' },
|
||||
{ label: 'Name', sortColumn: 'name' },
|
||||
{ label: 'User Count', sortColumn: 'userCount' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: 'Source' }] : []),
|
||||
{ label: 'Actions', hidden: true }
|
||||
{ label: m.friendly_name(), sortColumn: 'friendlyName' },
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.user_count(), sortColumn: 'userCount' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: m.source() }] : []),
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -63,7 +64,7 @@
|
||||
<Table.Cell>{item.userCount}</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? 'LDAP' : 'Local'}</Badge
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? m.ldap() : m.local()}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
@@ -72,18 +73,18 @@
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button aria-haspopup="true" size="icon" variant="ghost" builders={[builder]}>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle menu</span>
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item href="/settings/admin/user-groups/{item.id}"
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> {m.edit()}</DropdownMenu.Item
|
||||
>
|
||||
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
on:click={() => deleteUserGroup(item)}
|
||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||
><LucideTrash class="mr-2 h-4 w-4" />{m.delete()}</DropdownMenu.Item
|
||||
>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
@@ -35,8 +36,8 @@
|
||||
onRefresh={async (o) => (users = await userService.list(o))}
|
||||
{requestOptions}
|
||||
columns={[
|
||||
{ label: 'Name', sortColumn: 'firstName' },
|
||||
{ label: 'Email', sortColumn: 'email' }
|
||||
{ label: m.name(), sortColumn: 'firstName' },
|
||||
{ label: m.email(), sortColumn: 'email' }
|
||||
]}
|
||||
bind:selectedIds={selectedUserIds}
|
||||
{selectionDisabled}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import UserForm from './user-form.svelte';
|
||||
import UserList from './user-list.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let users = $state(data.users);
|
||||
@@ -23,7 +24,7 @@
|
||||
let success = true;
|
||||
await userService
|
||||
.create(user)
|
||||
.then(() => toast.success('User created successfully'))
|
||||
.then(() => toast.success(m.user_created_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
success = false;
|
||||
@@ -35,18 +36,18 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Users</title>
|
||||
<title>{m.users()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create User</Card.Title>
|
||||
<Card.Description>Add a new user to {$appConfigStore.appName}.</Card.Description>
|
||||
<Card.Title>{m.create_user()}</Card.Title>
|
||||
<Card.Description>{m.add_a_new_user_to_appname({ appName: $appConfigStore.appName })}.</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddUser}
|
||||
<Button on:click={() => (expandAddUser = true)}>Add User</Button>
|
||||
<Button on:click={() => (expandAddUser = true)}>{m.add_user()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddUser = false)}>
|
||||
<LucideMinus class="h-5 w-5" />
|
||||
@@ -65,7 +66,7 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Manage Users</Card.Title>
|
||||
<Card.Title>{m.manage_users()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserList {users} requestOptions={usersRequestOptions} />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { LucideChevronLeft } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import UserForm from '../user-form.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let user = $state({
|
||||
@@ -27,7 +28,7 @@
|
||||
async function updateUserGroups(userIds: string[]) {
|
||||
await userService
|
||||
.updateUserGroups(user.id, userIds)
|
||||
.then(() => toast.success('User groups updated successfully'))
|
||||
.then(() => toast.success(m.user_groups_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
});
|
||||
@@ -37,7 +38,7 @@
|
||||
let success = true;
|
||||
await userService
|
||||
.update(user.id, updatedUser)
|
||||
.then(() => toast.success('User updated successfully'))
|
||||
.then(() => toast.success(m.user_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
success = false;
|
||||
@@ -49,7 +50,7 @@
|
||||
async function updateCustomClaims() {
|
||||
await customClaimService
|
||||
.updateUserCustomClaims(user.id, user.customClaims)
|
||||
.then(() => toast.success('Custom claims updated successfully'))
|
||||
.then(() => toast.success(m.custom_claims_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
});
|
||||
@@ -58,33 +59,38 @@
|
||||
async function updateProfilePicture(image: File) {
|
||||
await userService
|
||||
.updateProfilePicture(user.id, image)
|
||||
.then(() => toast.success('Profile picture updated successfully. It may take a few minutes to update.'))
|
||||
.then(() => toast.success(m.profile_picture_updated_successfully()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
async function resetProfilePicture() {
|
||||
await userService
|
||||
.resetProfilePicture(user.id)
|
||||
.then(() => toast.success('Profile picture has been reset. It may take a few minutes to update.'))
|
||||
.then(() => toast.success(m.profile_picture_has_been_reset()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Details {user.firstName} {user.lastName}</title>
|
||||
<title
|
||||
>{m.user_details_firstname_lastname({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
})}</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/users"
|
||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||
><LucideChevronLeft class="h-5 w-5" /> {m.back()}</a
|
||||
>
|
||||
{#if !!user.ldapId}
|
||||
<Badge variant="default" class="">LDAP</Badge>
|
||||
<Badge variant="default" class="">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>General</Card.Title>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserForm existingUser={user} callback={updateUser} />
|
||||
@@ -104,8 +110,8 @@
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-groups"
|
||||
title="User Groups"
|
||||
description="Manage which groups this user belongs to."
|
||||
title={m.user_groups()}
|
||||
description={m.manage_which_groups_this_user_belongs_to()}
|
||||
>
|
||||
<UserGroupSelection
|
||||
bind:selectedGroupIds={user.userGroupIds}
|
||||
@@ -115,18 +121,18 @@
|
||||
<Button
|
||||
on:click={() => updateUserGroups(user.userGroupIds)}
|
||||
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||
type="submit">Save</Button
|
||||
type="submit">{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-custom-claims"
|
||||
title="Custom Claims"
|
||||
description="Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested."
|
||||
title={m.custom_claims()}
|
||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user()}
|
||||
>
|
||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button on:click={updateCustomClaims} type="submit">Save</Button>
|
||||
<Button on:click={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { User, UserCreate } from '$lib/types/user.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
@@ -35,7 +36,7 @@
|
||||
.max(30)
|
||||
.regex(
|
||||
/^[a-z0-9_@.-]+$/,
|
||||
"Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols"
|
||||
m.username_can_only_contain()
|
||||
),
|
||||
email: z.string().email(),
|
||||
isAdmin: z.boolean()
|
||||
@@ -57,19 +58,19 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<fieldset disabled={inputDisabled}>
|
||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput label="First name" bind:input={$inputs.firstName} />
|
||||
<FormInput label="Last name" bind:input={$inputs.lastName} />
|
||||
<FormInput label="Username" bind:input={$inputs.username} />
|
||||
<FormInput label="Email" bind:input={$inputs.email} />
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
<CheckboxWithLabel
|
||||
id="admin-privileges"
|
||||
label="Admin Privileges"
|
||||
description="Admins have full access to the admin panel."
|
||||
label={m.admin_privileges()}
|
||||
description={m.admins_have_full_access_to_the_admin_panel()}
|
||||
bind:checked={$inputs.isAdmin.value}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import Ellipsis from 'lucide-svelte/icons/ellipsis';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import OneTimeLinkModal from '$lib/components/one-time-link-modal.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
users = $bindable(),
|
||||
@@ -27,10 +28,10 @@
|
||||
|
||||
async function deleteUser(user: User) {
|
||||
openConfirmDialog({
|
||||
title: `Delete ${user.firstName} ${user.lastName}`,
|
||||
message: 'Are you sure you want to delete this user?',
|
||||
title: m.delete_firstname_lastname({firstName: user.firstName, lastName: user.lastName}),
|
||||
message: m.are_you_sure_you_want_to_delete_this_user(),
|
||||
confirm: {
|
||||
label: 'Delete',
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
@@ -39,7 +40,7 @@
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
toast.success('User deleted successfully');
|
||||
toast.success(m.user_deleted_successfully());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -51,13 +52,13 @@
|
||||
{requestOptions}
|
||||
onRefresh={async (options) => (users = await userService.list(options))}
|
||||
columns={[
|
||||
{ label: 'First name', sortColumn: 'firstName' },
|
||||
{ label: 'Last name', sortColumn: 'lastName' },
|
||||
{ label: 'Email', sortColumn: 'email' },
|
||||
{ label: 'Username', sortColumn: 'username' },
|
||||
{ label: 'Role', sortColumn: 'isAdmin' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: 'Source' }] : []),
|
||||
{ label: 'Actions', hidden: true }
|
||||
{ label: m.first_name(), sortColumn: 'firstName' },
|
||||
{ label: m.last_name(), sortColumn: 'lastName' },
|
||||
{ label: m.email(), sortColumn: 'email' },
|
||||
{ label: m.username(), sortColumn: 'username' },
|
||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: m.source()}] : []),
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -66,11 +67,11 @@
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
<Table.Cell>{item.username}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline">{item.isAdmin ? 'Admin' : 'User'}</Badge>
|
||||
<Badge variant="outline">{item.isAdmin ? m.admin() : m.user()}</Badge>
|
||||
</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? 'LDAP' : 'Local'}</Badge
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? m.ldap() : m.local()}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
@@ -78,20 +79,20 @@
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle menu</span>
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => (userIdToCreateOneTimeLink = item.id)}
|
||||
><LucideLink class="mr-2 h-4 w-4" />Login Code</DropdownMenu.Item
|
||||
><LucideLink class="mr-2 h-4 w-4" />{m.login_code()}</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item onclick={() => goto(`/settings/admin/users/${item.id}`)}
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> {m.edit()}</DropdownMenu.Item
|
||||
>
|
||||
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
onclick={() => deleteUser(item)}
|
||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||
><LucideTrash class="mr-2 h-4 w-4" />{m.delete()}</DropdownMenu.Item
|
||||
>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AuditLogList from './audit-log-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -8,14 +9,14 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Audit Log</title>
|
||||
<title>{m.audit_log()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Audit Log</Card.Title>
|
||||
<Card.Title>{m.audit_log()}</Card.Title>
|
||||
<Card.Description class="mt-1"
|
||||
>See your account activities from the last 3 months.</Card.Description
|
||||
>{m.see_your_account_activities_from_the_last_3_months()}</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { AuditLog } from '$lib/types/audit-log.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
@@ -27,12 +28,12 @@
|
||||
{requestOptions}
|
||||
onRefresh={async (options) => (auditLogs = await auditLogService.list(options))}
|
||||
columns={[
|
||||
{ label: 'Time', sortColumn: 'createdAt' },
|
||||
{ label: 'Event', sortColumn: 'event' },
|
||||
{ label: 'Approximate Location', sortColumn: 'city' },
|
||||
{ label: 'IP Address', sortColumn: 'ipAddress' },
|
||||
{ label: 'Device', sortColumn: 'device' },
|
||||
{ label: 'Client' }
|
||||
{ label: m.time(), sortColumn: 'createdAt' },
|
||||
{ label: m.event(), sortColumn: 'event' },
|
||||
{ label: m.approximate_location(), sortColumn: 'city' },
|
||||
{ label: m.ip_address(), sortColumn: 'ipAddress' },
|
||||
{ label: m.device(), sortColumn: 'device' },
|
||||
{ label: m.client() }
|
||||
]}
|
||||
withoutSearch
|
||||
>
|
||||
@@ -42,7 +43,7 @@
|
||||
<Badge variant="outline">{toFriendlyEventString(item.event)}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell
|
||||
>{item.city && item.country ? `${item.city}, ${item.country}` : 'Unknown'}</Table.Cell
|
||||
>{item.city && item.country ? `${item.city}, ${item.country}` : m.unknown()}</Table.Cell
|
||||
>
|
||||
<Table.Cell>{item.ipAddress}</Table.Cell>
|
||||
<Table.Cell>{item.device}</Table.Cell>
|
||||
|
||||
Reference in New Issue
Block a user