mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-30 03:06:37 +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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user