mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-19 17:23:33 +03:00
feat: modernize ui (#381)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.44.0",
|
"version": "0.45.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.44.0",
|
"version": "0.45.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"lucide-svelte": "^0.479.0",
|
"lucide-svelte": "^0.483.0",
|
||||||
"mode-watcher": "^0.5.1",
|
"mode-watcher": "^0.5.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-superforms": "^2.23.1",
|
"sveltekit-superforms": "^2.23.1",
|
||||||
@@ -3605,9 +3605,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/lucide-svelte": {
|
"node_modules/lucide-svelte": {
|
||||||
"version": "0.479.0",
|
"version": "0.483.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.479.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.483.0.tgz",
|
||||||
"integrity": "sha512-epCj6WL86ykxg7oCQTmPEth5e11pwJUzIfG9ROUsWsTP+WPtb3qat+VmAjfx/r4TRW7memTFcbTPvMrZvKthqw==",
|
"integrity": "sha512-MyMgEVLlFfPbyodGpkB+KCpyPkpjI7EKiFw1crA92B1ZXRK5hq5vTsGWAm9Nt3GAKHunoNc5MVsq3EOCz0DZSQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"lucide-svelte": "^0.479.0",
|
"lucide-svelte": "^0.483.0",
|
||||||
"mode-watcher": "^0.5.1",
|
"mode-watcher": "^0.5.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-superforms": "^2.23.1",
|
"sveltekit-superforms": "^2.23.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@config '../tailwind.config.ts';
|
@config '../tailwind.config.ts';
|
||||||
|
|
||||||
@@ -68,6 +68,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.8s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-bg-container {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 650px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-bg-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
animation: slide-bg-container 1.2s cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade in for content after the slide is mostly complete */
|
||||||
|
@keyframes delayed-fade {
|
||||||
|
0%,
|
||||||
|
40% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-delayed-fade {
|
||||||
|
animation: delayed-fade 1.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
@@ -77,7 +126,7 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
button{
|
button {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import { cn } from '$lib/utils/style';
|
import { cn } from '$lib/utils/style';
|
||||||
import { LucideChevronDown } from 'lucide-svelte';
|
import { LucideChevronDown, type Icon as IconType } from 'lucide-svelte';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import * as Card from './ui/card';
|
import * as Card from './ui/card';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
defaultExpanded = false,
|
defaultExpanded = false,
|
||||||
|
icon,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
defaultExpanded?: boolean;
|
defaultExpanded?: boolean;
|
||||||
|
icon?: typeof IconType;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -51,7 +53,12 @@
|
|||||||
<Card.Header class="cursor-pointer" onclick={toggleExpanded}>
|
<Card.Header class="cursor-pointer" onclick={toggleExpanded}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{title}</Card.Title>
|
<Card.Title class="flex items-center gap-2 text-xl font-semibold">
|
||||||
|
{#if icon}{@const Icon = icon}
|
||||||
|
<Icon class="text-primary/80 h-5 w-5" />
|
||||||
|
{/if}
|
||||||
|
{title}
|
||||||
|
</Card.Title>
|
||||||
{#if description}
|
{#if description}
|
||||||
<Card.Description>{description}</Card.Description>
|
<Card.Description>{description}</Card.Description>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -68,7 +75,7 @@
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<div transition:slide={{ duration: 200 }}>
|
<div transition:slide={{ duration: 200 }}>
|
||||||
<Card.Content>
|
<Card.Content class="bg-muted/20 pt-5">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
59
frontend/src/lib/components/fade-wrapper.svelte
Normal file
59
frontend/src/lib/components/fade-wrapper.svelte
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
delay = 50,
|
||||||
|
stagger = 150,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
delay?: number;
|
||||||
|
stagger?: number;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let containerNode: HTMLElement;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
page.route;
|
||||||
|
applyAnimationDelays();
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyAnimationDelays() {
|
||||||
|
if (containerNode) {
|
||||||
|
const childNodes = Array.from(containerNode.children);
|
||||||
|
childNodes.forEach((child, index) => {
|
||||||
|
// Skip comment nodes and text nodes
|
||||||
|
if (child.nodeType === 1) {
|
||||||
|
const itemDelay = delay + index * stagger;
|
||||||
|
(child as HTMLElement).style.setProperty('animation-delay', `${itemDelay}ms`);
|
||||||
|
console.log(itemDelay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<style>
|
||||||
|
/* Base styles */
|
||||||
|
.fade-wrapper {
|
||||||
|
display: contents;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply these styles to all children */
|
||||||
|
.fade-wrapper > * {
|
||||||
|
animation-fill-mode: both;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
animation-delay: calc(var(--animation-delay, 0ms) + 0.1s);
|
||||||
|
animation: fadeIn 0.8s ease-out forwards;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="fade-wrapper" bind:this={containerNode}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||||
import { openConfirmDialog } from '../confirm-dialog';
|
import { openConfirmDialog } from '../confirm-dialog';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import type UserService from '$lib/services/user-service';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
userId,
|
userId,
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
await updateCallback(file).catch(() => {
|
await updateCallback(file).catch(() => {
|
||||||
imageDataURL = `/api/users/${userId}/profile-picture.png}`;
|
imageDataURL = `/api/users/${userId}/profile-picture.png`;
|
||||||
});
|
});
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -55,47 +56,23 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex gap-5">
|
<div class="flex flex-col items-center gap-6 sm:flex-row">
|
||||||
<div class="flex w-full flex-col justify-between gap-5 sm:flex-row">
|
<div class="shrink-0">
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-semibold">{m.profile_picture()}</h3>
|
|
||||||
{#if isLdapUser}
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">
|
|
||||||
{m.profile_picture_is_managed_by_ldap_server()}
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">
|
|
||||||
{m.click_profile_picture_to_upload_custom()}
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">{m.image_should_be_in_format()}</p>
|
|
||||||
{/if}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="mt-5"
|
|
||||||
on:click={onReset}
|
|
||||||
disabled={isLoading || isLdapUser}
|
|
||||||
>
|
|
||||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
|
||||||
{m.reset_to_default()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{#if isLdapUser}
|
{#if isLdapUser}
|
||||||
<Avatar.Root class="h-24 w-24">
|
<Avatar.Root class="h-24 w-24">
|
||||||
<Avatar.Image class="object-cover" src={imageDataURL} />
|
<Avatar.Image class="object-cover" src={imageDataURL} />
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<FileInput
|
<FileInput
|
||||||
id="profile-picture-input"
|
id="profile-picture-input"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
onchange={onImageChange}
|
onchange={onImageChange}
|
||||||
>
|
>
|
||||||
<div class="group relative h-28 w-28 rounded-full">
|
<div class="group relative h-24 w-24 rounded-full">
|
||||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
class="object-cover group-hover:opacity-10 {isLoading ? 'opacity-10' : ''}"
|
class="object-cover group-hover:opacity-30 {isLoading ? 'opacity-30' : ''}"
|
||||||
src={imageDataURL}
|
src={imageDataURL}
|
||||||
/>
|
/>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
@@ -103,14 +80,29 @@
|
|||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||||
{:else}
|
{:else}
|
||||||
<LucideUpload
|
<LucideUpload class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FileInput>
|
</FileInput>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grow">
|
||||||
|
<h3 class="font-medium">{m.profile_picture()}</h3>
|
||||||
|
{#if isLdapUser}
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
{m.profile_picture_is_managed_by_ldap_server()}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
{m.click_profile_picture_to_upload_custom()}
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground mb-2 text-sm">{m.image_should_be_in_format()}</p>
|
||||||
|
<Button variant="outline" size="sm" on:click={onReset} disabled={isLoading || isLdapUser}>
|
||||||
|
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||||
|
{m.reset_to_default()}
|
||||||
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
75
frontend/src/lib/components/glass-row-item.svelte
Normal file
75
frontend/src/lib/components/glass-row-item.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/components/ui/tooltip';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { LucideCalendar, LucidePencil, LucideTrash, type Icon as IconType } from 'lucide-svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
icon,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
label,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
icon: typeof IconType;
|
||||||
|
onRename: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
description?: string;
|
||||||
|
label?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-card hover:bg-muted/50 group rounded-lg p-3 transition-colors">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="bg-primary/10 text-primary mt-1 rounded-lg p-2">
|
||||||
|
{#if icon}{@const Icon = icon}
|
||||||
|
<Icon class="h-5 w-5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="font-medium">{label}</p>
|
||||||
|
</div>
|
||||||
|
{#if description}
|
||||||
|
<div class="text-muted-foreground mt-1 flex items-center text-xs">
|
||||||
|
<LucideCalendar class="mr-1 h-3 w-3" />
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
on:click={onRename}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-8 w-8"
|
||||||
|
aria-label={m.rename()}
|
||||||
|
>
|
||||||
|
<LucidePencil class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{m.rename()}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
on:click={onDelete}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="hover:bg-destructive/10 hover:text-destructive h-8 w-8"
|
||||||
|
aria-label={m.delete()}
|
||||||
|
>
|
||||||
|
<LucideTrash class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{m.delete()}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -20,10 +20,15 @@
|
|||||||
>
|
>
|
||||||
<div class="flex h-16 items-center">
|
<div class="flex h-16 items-center">
|
||||||
{#if !isAuthPage}
|
{#if !isAuthPage}
|
||||||
<Logo class="mr-3 h-8 w-8" />
|
<a
|
||||||
<h1 class="text-lg font-medium" data-testid="application-name">
|
href="/settings/account"
|
||||||
|
class="flex items-center gap-3 transition-opacity hover:opacity-80"
|
||||||
|
>
|
||||||
|
<Logo class="h-8 w-8" />
|
||||||
|
<h1 class="text-lg font-semibold tracking-tight" data-testid="application-name">
|
||||||
{$appConfigStore.appName}
|
{$appConfigStore.appName}
|
||||||
</h1>
|
</h1>
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { cn } from '$lib/utils/style';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import * as Card from './ui/card';
|
import * as Card from './ui/card';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
children,
|
children,
|
||||||
showAlternativeSignInMethodButton = false
|
showAlternativeSignInMethodButton = false,
|
||||||
|
animate = false
|
||||||
}: {
|
}: {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
showAlternativeSignInMethodButton?: boolean;
|
showAlternativeSignInMethodButton?: boolean;
|
||||||
|
animate?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Desktop -->
|
<!-- Desktop with sliding reveal animation -->
|
||||||
<div class="hidden h-screen items-center text-center lg:flex">
|
<div class="hidden h-screen items-center overflow-hidden text-center lg:flex">
|
||||||
<div class="h-full min-w-[650px] p-16 {showAlternativeSignInMethodButton ? 'pb-0' : ''}">
|
<!-- Content area that fades in after background slides -->
|
||||||
<div class="flex h-full flex-col">
|
<div
|
||||||
<div class="flex flex-grow flex-col items-center justify-center">
|
class="relative z-10 flex h-full w-[650px] p-16 {cn(
|
||||||
|
showAlternativeSignInMethodButton && 'pb-0',
|
||||||
|
animate && 'animate-delayed-fade'
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||||
|
<div class="relative flex flex-grow flex-col items-center justify-center overflow-auto">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
{#if showAlternativeSignInMethodButton}
|
{#if showAlternativeSignInMethodButton}
|
||||||
<div class="mb-4 flex justify-center">
|
<div
|
||||||
|
class="mb-4 flex items-center justify-center"
|
||||||
|
style={animate ? 'animation-delay: 1000ms;' : ''}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={page.url.pathname == '/login'
|
href={page.url.pathname == '/login'
|
||||||
? '/login/alternative'
|
? '/login/alternative'
|
||||||
: `/login/alternative?redirect=${encodeURIComponent(
|
: `/login/alternative?redirect=${encodeURIComponent(
|
||||||
page.url.pathname + page.url.search
|
page.url.pathname + page.url.search
|
||||||
)}`}
|
)}`}
|
||||||
class="text-muted-foreground text-xs"
|
class="text-muted-foreground text-xs transition-colors hover:underline"
|
||||||
>
|
>
|
||||||
{m.dont_have_access_to_your_passkey()}
|
{m.dont_have_access_to_your_passkey()}
|
||||||
</a>
|
</a>
|
||||||
@@ -36,18 +48,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Background image with slide animation -->
|
||||||
|
<div class="{cn(animate && 'animate-slide-bg-container')} absolute bottom-0 right-0 top-0 z-0">
|
||||||
<img
|
<img
|
||||||
src="/api/application-configuration/background-image"
|
src="/api/application-configuration/background-image"
|
||||||
class="h-screen w-[calc(100vw-650px)] rounded-l-[60px] object-cover"
|
class="h-screen rounded-l-[60px] object-cover {animate ? 'w-full' : 'w-[calc(100vw-650px)]'}"
|
||||||
alt={m.login_background()}
|
alt={m.login_background()}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile -->
|
<!-- Mobile -->
|
||||||
<div
|
<div
|
||||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
||||||
>
|
>
|
||||||
<Card.Root class="mx-3">
|
<Card.Root class="mx-3 w-full max-w-md" style={animate ? 'animation-delay: 200ms;' : ''}>
|
||||||
<Card.CardContent
|
<Card.CardContent
|
||||||
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''}"
|
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''}"
|
||||||
>
|
>
|
||||||
@@ -59,7 +75,7 @@
|
|||||||
: `/login/alternative?redirect=${encodeURIComponent(
|
: `/login/alternative?redirect=${encodeURIComponent(
|
||||||
page.url.pathname + page.url.search
|
page.url.pathname + page.url.search
|
||||||
)}`}
|
)}`}
|
||||||
class="text-muted-foreground mt-7 flex justify-center text-xs"
|
class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline"
|
||||||
>
|
>
|
||||||
{m.dont_have_access_to_your_passkey()}
|
{m.dont_have_access_to_your_passkey()}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
|
||||||
import { cn } from '$lib/utils/style.js';
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('p-6 pt-0', className)} {...$$restProps}>
|
<div class={cn('bg-muted/20 p-6 pt-5 peer-[.card-header]:border-t', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class={cn('text-sm text-muted-foreground mt-1', className)} {...$$restProps}>
|
<p class={cn('text-muted-foreground mt-1 text-sm', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
|
||||||
import { cn } from '$lib/utils/style.js';
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
<div class={cn('card-header peer flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={tag}
|
this={tag}
|
||||||
class={cn('text-xl font-semibold leading-none tracking-tight', className)}
|
class={cn('flex items-center gap-2 text-xl font-semibold leading-none tracking-tight', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
class={cn('bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -78,15 +78,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{m.sign_in_to({name: client.name})}</title>
|
<title>{m.sign_in_to({ name: client.name })}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if client == null}
|
{#if client == null}
|
||||||
<p>{m.client_not_found()}</p>
|
<p>{m.client_not_found()}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<SignInWrapper showAlternativeSignInMethodButton>
|
<SignInWrapper animate showAlternativeSignInMethodButton>
|
||||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.sign_in_to({name: 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}
|
{#if errorMessage}
|
||||||
<p class="text-muted-foreground mb-10 mt-2">
|
<p class="text-muted-foreground mb-10 mt-2">
|
||||||
{errorMessage}.
|
{errorMessage}.
|
||||||
@@ -110,7 +112,11 @@
|
|||||||
<Card.Content data-testid="scopes">
|
<Card.Content data-testid="scopes">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#if scope!.includes('email')}
|
{#if scope!.includes('email')}
|
||||||
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
|
<ScopeItem
|
||||||
|
icon={LucideMail}
|
||||||
|
name={m.email()}
|
||||||
|
description={m.view_your_email_address()}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if scope!.includes('profile')}
|
{#if scope!.includes('profile')}
|
||||||
<ScopeItem
|
<ScopeItem
|
||||||
@@ -132,7 +138,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex w-full justify-stretch gap-2">
|
<div class="flex w-full justify-stretch gap-2">
|
||||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">{m.cancel()}</Button>
|
<Button onclick={() => history.back()} class="w-full" variant="secondary">{m.cancel()}</Button
|
||||||
|
>
|
||||||
{#if !errorMessage}
|
{#if !errorMessage}
|
||||||
<Button class="w-full" {isLoading} on:click={authorize}>{m.sign_in()}</Button>
|
<Button class="w-full" {isLoading} on:click={authorize}>{m.sign_in()}</Button>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -36,12 +36,12 @@
|
|||||||
<title>{m.sign_in()}</title>
|
<title>{m.sign_in()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<SignInWrapper showAlternativeSignInMethodButton>
|
<SignInWrapper animate showAlternativeSignInMethodButton>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||||
{m.sign_in_to_appname({ appName: $appConfigStore.appName})}
|
{m.sign_in_to_appname({ appName: $appConfigStore.appName })}
|
||||||
</h1>
|
</h1>
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="text-muted-foreground mt-2" in:fade>
|
<p class="text-muted-foreground mt-2" in:fade>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<title>{m.logout()}</title>
|
<title>{m.logout()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<SignInWrapper>
|
<SignInWrapper animate>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="bg-muted rounded-2xl p-3">
|
<div class="bg-muted rounded-2xl p-3">
|
||||||
<Logo class="h-10 w-10" />
|
<Logo class="h-10 w-10" />
|
||||||
@@ -35,7 +35,9 @@
|
|||||||
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.sign_out()}</h1>
|
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.sign_out()}</h1>
|
||||||
|
|
||||||
<p class="text-muted-foreground mt-2">
|
<p class="text-muted-foreground mt-2">
|
||||||
{@html m.do_you_want_to_sign_out_of_pocketid_with_the_account({ username: $userStore?.username ?? '' })}
|
{@html m.do_you_want_to_sign_out_of_pocketid_with_the_account({
|
||||||
|
username: $userStore?.username ?? ''
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||||
<Button class="w-full" variant="secondary" onclick={() => history.back()}>{m.cancel()}</Button>
|
<Button class="w-full" variant="secondary" onclick={() => history.back()}>{m.cancel()}</Button>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/state';
|
||||||
import userStore from '$lib/stores/user-store';
|
import FadeWrapper from '$lib/components/fade-wrapper.svelte';
|
||||||
import { LucideExternalLink } from 'lucide-svelte';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import type { LayoutData } from './$types';
|
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import userStore from '$lib/stores/user-store';
|
||||||
|
import { LucideExternalLink, LucideSettings } from 'lucide-svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
children,
|
children,
|
||||||
@@ -14,38 +16,51 @@
|
|||||||
data: LayoutData;
|
data: LayoutData;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const { versionInformation } = data;
|
const { versionInformation, user } = data;
|
||||||
|
|
||||||
let links = $state([
|
const links = [
|
||||||
{ href: '/settings/account', label: m.my_account() },
|
{ href: '/settings/account', label: m.my_account() },
|
||||||
{ href: '/settings/audit-log', label: m.audit_log() }
|
{ href: '/settings/audit-log', label: m.audit_log() },
|
||||||
]);
|
];
|
||||||
|
|
||||||
if ($userStore?.isAdmin) {
|
const adminLinks = [
|
||||||
links = [
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
...links,
|
|
||||||
{ href: '/settings/admin/users', label: m.users() },
|
{ href: '/settings/admin/users', label: m.users() },
|
||||||
{ href: '/settings/admin/user-groups', label: m.user_groups() },
|
{ href: '/settings/admin/user-groups', label: m.user_groups() },
|
||||||
{ href: '/settings/admin/oidc-clients', label: m.oidc_clients() },
|
{ href: '/settings/admin/oidc-clients', label: m.oidc_clients() },
|
||||||
{ href: '/settings/admin/api-keys', label: m.api_keys() },
|
{ href: '/settings/admin/api-keys', label: m.api_keys() },
|
||||||
{ href: '/settings/admin/application-configuration', label: m.application_configuration() }
|
{ href: '/settings/admin/application-configuration', label: m.application_configuration() }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (user?.isAdmin || $userStore?.isAdmin) {
|
||||||
|
links.push(...adminLinks);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="bg-muted/40 flex min-h-[calc(100vh-64px)] w-full flex-col justify-between">
|
<div class="bg-muted/40 flex min-h-[calc(100vh-64px)] w-full flex-col justify-between">
|
||||||
<main
|
<main
|
||||||
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
|
in:fade={{ duration: 300 }}
|
||||||
|
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-8 gap-y-8 overflow-hidden p-4 md:p-8 lg:flex-row"
|
||||||
>
|
>
|
||||||
<div class="min-w-[200px] xl:min-w-[250px]">
|
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||||
|
<div in:fly={{ x: -15, duration: 300 }} class="sticky top-6">
|
||||||
<div class="mx-auto grid w-full gap-2">
|
<div class="mx-auto grid w-full gap-2">
|
||||||
<h1 class="mb-5 text-3xl font-semibold">{m.settings()}</h1>
|
<h1 class="mb-4 flex items-center gap-2 text-2xl font-semibold">
|
||||||
|
<LucideSettings class="h-5 w-5" />
|
||||||
|
{m.settings()}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="text-muted-foreground grid gap-4 text-sm">
|
<nav class="text-muted-foreground grid gap-2 text-sm">
|
||||||
{#each links as { href, label }}
|
{#each links as { href, label }, i}
|
||||||
<a {href} class={$page.url.pathname.startsWith(href) ? 'text-primary font-bold' : ''}>
|
<a
|
||||||
|
{href}
|
||||||
|
class={`animate-fade-in ${
|
||||||
|
page.url.pathname.startsWith(href)
|
||||||
|
? 'text-primary bg-card rounded-md px-3 py-1.5 font-medium shadow-sm transition-all'
|
||||||
|
: 'hover:text-foreground hover:bg-muted/70 rounded-md px-3 py-1.5 transition-all hover:-translate-y-[2px] hover:shadow-sm'
|
||||||
|
}`}
|
||||||
|
style={`animation-delay: ${150 + i * 75}ms;`}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -53,21 +68,27 @@
|
|||||||
<a
|
<a
|
||||||
href="https://github.com/pocket-id/pocket-id/releases/latest"
|
href="https://github.com/pocket-id/pocket-id/releases/latest"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex items-center gap-2"
|
class="animate-fade-in hover:text-foreground hover:bg-muted/70 mt-1 flex items-center gap-2 rounded-md px-3 py-1.5 text-orange-500 transition-all hover:-translate-y-[2px] hover:shadow-sm"
|
||||||
|
style={`animation-delay: ${150 + links.length * 75}ms;`}
|
||||||
>
|
>
|
||||||
{m.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>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col gap-5 overflow-x-hidden">
|
</div>
|
||||||
|
<div class="flex w-full flex-col gap-4 overflow-hidden">
|
||||||
|
<FadeWrapper>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</FadeWrapper>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div class="flex flex-col items-center">
|
<div class="animate-fade-in flex flex-col items-center" style="animation-delay: 400ms;">
|
||||||
<p class="text-muted-foreground py-3 text-xs">
|
<p class="text-muted-foreground py-3 text-xs">
|
||||||
{m.powered_by()} <a
|
{m.powered_by()}
|
||||||
class="text-foreground"
|
<a
|
||||||
|
class="text-foreground transition-all hover:underline"
|
||||||
href="https://github.com/pocket-id/pocket-id"
|
href="https://github.com/pocket-id/pocket-id"
|
||||||
target="_blank">Pocket ID</a
|
target="_blank">Pocket ID</a
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,9 +10,14 @@
|
|||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { UserCreate } from '$lib/types/user.type';
|
||||||
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
|
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import { LucideAlertTriangle } from 'lucide-svelte';
|
import {
|
||||||
|
KeyRound,
|
||||||
|
Languages,
|
||||||
|
LucideAlertTriangle,
|
||||||
|
RectangleEllipsis,
|
||||||
|
UserCog
|
||||||
|
} from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import ProfilePictureSettings from '../../../lib/components/form/profile-picture-settings.svelte';
|
|
||||||
import AccountForm from './account-form.svelte';
|
import AccountForm from './account-form.svelte';
|
||||||
import LocalePicker from './locale-picker.svelte';
|
import LocalePicker from './locale-picker.svelte';
|
||||||
import LoginCodeModal from './login-code-modal.svelte';
|
import LoginCodeModal from './login-code-modal.svelte';
|
||||||
@@ -28,15 +33,6 @@
|
|||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
const webauthnService = new WebAuthnService();
|
const webauthnService = new WebAuthnService();
|
||||||
|
|
||||||
async function resetProfilePicture() {
|
|
||||||
await userService
|
|
||||||
.resetCurrentUserProfilePicture()
|
|
||||||
.then(() =>
|
|
||||||
toast.success('Profile picture has been reset. It may take a few minutes to update.')
|
|
||||||
)
|
|
||||||
.catch(axiosErrorToast);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateAccount(user: UserCreate) {
|
async function updateAccount(user: UserCreate) {
|
||||||
let success = true;
|
let success = true;
|
||||||
await userService
|
await userService
|
||||||
@@ -50,13 +46,6 @@
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateProfilePicture(image: File) {
|
|
||||||
await userService
|
|
||||||
.updateCurrentUsersProfilePicture(image)
|
|
||||||
.then(() => toast.success(m.profile_picture_updated_successfully()))
|
|
||||||
.catch(axiosErrorToast);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createPasskey() {
|
async function createPasskey() {
|
||||||
try {
|
try {
|
||||||
const opts = await webauthnService.getRegistrationOptions();
|
const opts = await webauthnService.getRegistrationOptions();
|
||||||
@@ -76,56 +65,67 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if passkeys.length == 0}
|
{#if passkeys.length == 0}
|
||||||
<Alert.Root variant="warning">
|
<Alert.Root variant="warning" class="flex gap-3">
|
||||||
<LucideAlertTriangle class="size-4" />
|
<LucideAlertTriangle class="size-4" />
|
||||||
<Alert.Title>{m.passkey_missing()}</Alert.Title>
|
<div>
|
||||||
<Alert.Description
|
<Alert.Title class="font-semibold">{m.passkey_missing()}</Alert.Title>
|
||||||
>{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()}</Alert.Description
|
<Alert.Description class="text-sm">
|
||||||
>
|
{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()}
|
||||||
|
</Alert.Description>
|
||||||
|
</div>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
{:else if passkeys.length == 1}
|
{:else if passkeys.length == 1}
|
||||||
<Alert.Root variant="warning" dismissibleId="single-passkey">
|
<Alert.Root variant="warning" dismissibleId="single-passkey" class="flex gap-3">
|
||||||
<LucideAlertTriangle class="size-4" />
|
<LucideAlertTriangle class="size-4" />
|
||||||
<Alert.Title>{m.single_passkey_configured()}</Alert.Title>
|
<div>
|
||||||
<Alert.Description>{m.it_is_recommended_to_add_more_than_one_passkey()}</Alert.Description>
|
<Alert.Title class="font-semibold">{m.single_passkey_configured()}</Alert.Title>
|
||||||
|
<Alert.Description class="text-sm">
|
||||||
|
{m.it_is_recommended_to_add_more_than_one_passkey()}
|
||||||
|
</Alert.Description>
|
||||||
|
</div>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Account details card -->
|
||||||
<fieldset
|
<fieldset
|
||||||
disabled={!$appConfigStore.allowOwnAccountEdit ||
|
disabled={!$appConfigStore.allowOwnAccountEdit ||
|
||||||
(!!account.ldapId && $appConfigStore.ldapEnabled)}
|
(!!account.ldapId && $appConfigStore.ldapEnabled)}
|
||||||
>
|
>
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.account_details()}</Card.Title>
|
<Card.Title>
|
||||||
|
<UserCog class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.account_details()}
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<AccountForm {account} callback={updateAccount} />
|
<AccountForm
|
||||||
|
{account}
|
||||||
|
userId={account.id}
|
||||||
|
callback={updateAccount}
|
||||||
|
isLdapUser={!!account.ldapId}
|
||||||
|
/>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<Card.Root>
|
<!-- Passkey management card -->
|
||||||
<Card.Content class="pt-6">
|
<div>
|
||||||
<ProfilePictureSettings
|
<Card.Root>
|
||||||
userId={account.id}
|
|
||||||
isLdapUser={!!account.ldapId}
|
|
||||||
updateCallback={updateProfilePicture}
|
|
||||||
resetCallback={resetProfilePicture}
|
|
||||||
/>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.passkeys()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description class="mt-1">
|
<KeyRound class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.passkeys()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
||||||
</Card.Description>
|
</Card.Description>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" class="ml-3" on:click={createPasskey}>{m.add_passkey()}</Button>
|
<Button variant="outline" class="ml-3" on:click={createPasskey}>
|
||||||
|
{m.add_passkey()}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
{#if passkeys.length != 0}
|
{#if passkeys.length != 0}
|
||||||
@@ -133,37 +133,51 @@
|
|||||||
<PasskeyList bind:passkeys />
|
<PasskeyList bind:passkeys />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<!-- Login code card -->
|
||||||
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.login_code()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description class="mt-1">
|
<RectangleEllipsis class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.login_code()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
{m.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>
|
</Card.Description>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}
|
<Button variant="outline" on:click={() => (showLoginCodeModal = true)}>
|
||||||
>{m.create()}</Button
|
{m.create()}
|
||||||
>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<!-- Language selection card -->
|
||||||
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.language()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description class="mt-1">
|
<Languages class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.language()}
|
||||||
|
</Card.Title>
|
||||||
|
|
||||||
|
<Card.Description>
|
||||||
{m.select_the_language_you_want_to_use()}
|
{m.select_the_language_you_want_to_use()}
|
||||||
</Card.Description>
|
</Card.Description>
|
||||||
</div>
|
</div>
|
||||||
<LocalePicker />
|
<LocalePicker />
|
||||||
</div>
|
</div>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<RenamePasskeyModal
|
<RenamePasskeyModal
|
||||||
bind:passkey={passkeyToRename}
|
bind:passkey={passkeyToRename}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
|
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import UserService from '$lib/services/user-service';
|
||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { UserCreate } from '$lib/types/user.type';
|
||||||
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
import { BookUser } from 'lucide-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
callback,
|
callback,
|
||||||
account
|
account,
|
||||||
|
userId,
|
||||||
|
isLdapUser = false
|
||||||
}: {
|
}: {
|
||||||
account: UserCreate;
|
account: UserCreate;
|
||||||
|
userId: string;
|
||||||
callback: (user: UserCreate) => Promise<boolean>;
|
callback: (user: UserCreate) => Promise<boolean>;
|
||||||
|
isLdapUser?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
const userService = new UserService();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().min(1).max(50),
|
lastName: z.string().min(1).max(50),
|
||||||
@@ -23,27 +34,51 @@
|
|||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
.max(30)
|
.max(30)
|
||||||
.regex(
|
.regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()),
|
||||||
/^[a-z0-9_@.-]+$/,
|
|
||||||
m.username_can_only_contain()
|
|
||||||
),
|
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
isAdmin: z.boolean()
|
isAdmin: z.boolean()
|
||||||
});
|
});
|
||||||
type FormSchema = typeof formSchema;
|
type FormSchema = typeof formSchema;
|
||||||
|
|
||||||
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const data = form.validate();
|
const data = form.validate();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
await callback(data);
|
await callback(data);
|
||||||
// Reset form if user was successfully created
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateProfilePicture(image: File) {
|
||||||
|
await userService
|
||||||
|
.updateProfilePicture(userId, image)
|
||||||
|
.then(() => toast.success(m.profile_picture_updated_successfully()))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetProfilePicture() {
|
||||||
|
await userService
|
||||||
|
.resetProfilePicture(userId)
|
||||||
|
.then(() => toast.success(m.profile_picture_has_been_reset()))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={onSubmit}>
|
<form onsubmit={onSubmit} class="space-y-6">
|
||||||
|
<!-- Profile Picture Section -->
|
||||||
|
<ProfilePictureSettings
|
||||||
|
{userId}
|
||||||
|
{isLdapUser}
|
||||||
|
updateCallback={updateProfilePicture}
|
||||||
|
resetCallback={resetProfilePicture}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- User Information -->
|
||||||
|
<div>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row">
|
<div class="flex flex-col gap-3 sm:flex-row">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||||
@@ -60,7 +95,9 @@
|
|||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 flex justify-end">
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-2">
|
||||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import GlassRowItem from '$lib/components/glass-row-item.svelte';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import WebauthnService from '$lib/services/webauthn-service';
|
import WebauthnService from '$lib/services/webauthn-service';
|
||||||
import type { Passkey } from '$lib/types/passkey.type';
|
import type { Passkey } from '$lib/types/passkey.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte';
|
import { LucideKeyRound } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
|
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
|
||||||
|
|
||||||
@@ -37,38 +36,18 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="space-y-3">
|
||||||
{#each passkeys as passkey, i}
|
{#each passkeys as passkey}
|
||||||
<div class="flex justify-between">
|
<GlassRowItem
|
||||||
<div class="flex items-center">
|
label={passkey.name}
|
||||||
<LucideKeyRound class="mr-4 inline h-6 w-6" />
|
description={m.added_on() + ' ' + new Date(passkey.createdAt).toLocaleDateString()}
|
||||||
<div>
|
icon={LucideKeyRound}
|
||||||
<p>{passkey.name}</p>
|
onRename={() => (passkeyToRename = passkey)}
|
||||||
<p class="text-xs text-muted-foreground">
|
onDelete={() => deletePasskey(passkey)}
|
||||||
{m.added_on()} {new Date(passkey.createdAt).toLocaleDateString()}
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
on:click={() => (passkeyToRename = passkey)}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
aria-label={m.rename()}><LucidePencil class="h-3 w-3" /></Button
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
on:click={() => deletePasskey(passkey)}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
aria-label={m.delete()}><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if i !== passkeys.length - 1}
|
|
||||||
<Separator class="my-2" />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RenamePasskeyModal
|
<RenamePasskeyModal
|
||||||
bind:passkey={passkeyToRename}
|
bind:passkey={passkeyToRename}
|
||||||
callback={async () => (passkeys = await webauthnService.listCredentials())}
|
callback={async () => (passkeys = await webauthnService.listCredentials())}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import ApiKeyService from '$lib/services/api-key-service';
|
import ApiKeyService from '$lib/services/api-key-service';
|
||||||
import type { ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type';
|
import type { ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideMinus } from 'lucide-svelte';
|
import { LucideMinus, ShieldEllipsis, ShieldPlus } from 'lucide-svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import ApiKeyDialog from './api-key-dialog.svelte';
|
import ApiKeyDialog from './api-key-dialog.svelte';
|
||||||
import ApiKeyForm from './api-key-form.svelte';
|
import ApiKeyForm from './api-key-form.svelte';
|
||||||
import ApiKeyList from './api-key-list.svelte';
|
import ApiKeyList from './api-key-list.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let apiKeys = $state(data.apiKeys);
|
let apiKeys = $state(data.apiKeys);
|
||||||
@@ -39,11 +39,15 @@
|
|||||||
<title>{m.api_keys()}</title>
|
<title>{m.api_keys()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.create_api_key()}</Card.Title>
|
<Card.Title>
|
||||||
|
<ShieldPlus class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.create_api_key()}
|
||||||
|
</Card.Title>
|
||||||
<Card.Description>{m.add_a_new_api_key_for_programmatic_access()}</Card.Description>
|
<Card.Description>{m.add_a_new_api_key_for_programmatic_access()}</Card.Description>
|
||||||
</div>
|
</div>
|
||||||
{#if !expandAddApiKey}
|
{#if !expandAddApiKey}
|
||||||
@@ -62,15 +66,21 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root class="mt-6">
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.manage_api_keys()}</Card.Title>
|
<Card.Title>
|
||||||
|
<ShieldEllipsis class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.manage_api_keys()}
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ApiKeyDialog bind:apiKeyResponse />
|
<ApiKeyDialog bind:apiKeyResponse />
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
|
import { LucideImage, Mail, SlidersHorizontal, UserSearch } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||||
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
||||||
import UpdateApplicationImages from './update-application-images.svelte';
|
import UpdateApplicationImages from './update-application-images.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let appConfig = $state(data.appConfig);
|
let appConfig = $state(data.appConfig);
|
||||||
@@ -56,26 +57,41 @@
|
|||||||
<title>{m.application_configuration()}</title>
|
<title>{m.application_configuration()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<CollapsibleCard id="application-configuration-general" title={m.general()} defaultExpanded>
|
<div>
|
||||||
|
<CollapsibleCard
|
||||||
|
id="application-configuration-general"
|
||||||
|
icon={SlidersHorizontal}
|
||||||
|
title={m.general()}
|
||||||
|
defaultExpanded
|
||||||
|
>
|
||||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CollapsibleCard
|
<div>
|
||||||
|
<CollapsibleCard
|
||||||
id="application-configuration-email"
|
id="application-configuration-email"
|
||||||
|
icon={Mail}
|
||||||
title={m.email()}
|
title={m.email()}
|
||||||
description={m.enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location()}
|
description={m.enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location()}
|
||||||
>
|
>
|
||||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CollapsibleCard
|
<div>
|
||||||
|
<CollapsibleCard
|
||||||
id="application-configuration-ldap"
|
id="application-configuration-ldap"
|
||||||
|
icon={UserSearch}
|
||||||
title={m.ldap()}
|
title={m.ldap()}
|
||||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||||
>
|
>
|
||||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CollapsibleCard id="application-configuration-images" title={m.images()}>
|
<div>
|
||||||
|
<CollapsibleCard id="application-configuration-images" icon={LucideImage} title={m.images()}>
|
||||||
<UpdateApplicationImages callback={updateImages} />
|
<UpdateApplicationImages callback={updateImages} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import OIDCService from '$lib/services/oidc-service';
|
import OIDCService from '$lib/services/oidc-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import clientSecretStore from '$lib/stores/client-secret-store';
|
import clientSecretStore from '$lib/stores/client-secret-store';
|
||||||
import type { OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
import type { OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideMinus } from 'lucide-svelte';
|
import { LucideMinus, ShieldCheck, ShieldPlus } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import OIDCClientForm from './oidc-client-form.svelte';
|
import OIDCClientForm from './oidc-client-form.svelte';
|
||||||
import OIDCClientList from './oidc-client-list.svelte';
|
import OIDCClientList from './oidc-client-list.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let clients = $state(data.clients);
|
let clients = $state(data.clients);
|
||||||
@@ -43,12 +43,20 @@
|
|||||||
<title>{m.oidc_clients()}</title>
|
<title>{m.oidc_clients()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.create_oidc_client()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description>{m.add_a_new_oidc_client_to_appname({ appName: $appConfigStore.appName})}</Card.Description>
|
<ShieldPlus class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.create_oidc_client()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description
|
||||||
|
>{m.add_a_new_oidc_client_to_appname({
|
||||||
|
appName: $appConfigStore.appName
|
||||||
|
})}</Card.Description
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if !expandAddClient}
|
{#if !expandAddClient}
|
||||||
<Button on:click={() => (expandAddClient = true)}>{m.add_oidc_client()}</Button>
|
<Button on:click={() => (expandAddClient = true)}>{m.add_oidc_client()}</Button>
|
||||||
@@ -66,13 +74,19 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.manage_oidc_clients()}</Card.Title>
|
<Card.Title>
|
||||||
|
<ShieldCheck class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.manage_oidc_clients()}
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<OIDCClientList {clients} requestOptions={clientsRequestOptions} />
|
<OIDCClientList {clients} requestOptions={clientsRequestOptions} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
async function deleteClient(client: OidcClient) {
|
async function deleteClient(client: OidcClient) {
|
||||||
openConfirmDialog({
|
openConfirmDialog({
|
||||||
title: m.delete_name({name: client.name}),
|
title: m.delete_name({ name: client.name }),
|
||||||
message: m.are_you_sure_you_want_to_delete_this_oidc_client(),
|
message: m.are_you_sure_you_want_to_delete_this_oidc_client(),
|
||||||
confirm: {
|
confirm: {
|
||||||
label: m.delete(),
|
label: m.delete(),
|
||||||
@@ -58,13 +58,15 @@
|
|||||||
{#snippet rows({ item })}
|
{#snippet rows({ item })}
|
||||||
<Table.Cell class="w-8 font-medium">
|
<Table.Cell class="w-8 font-medium">
|
||||||
{#if item.hasLogo}
|
{#if item.hasLogo}
|
||||||
|
<div class="bg-secondary rounded-2xl p-3">
|
||||||
<div class="h-8 w-8">
|
<div class="h-8 w-8">
|
||||||
<img
|
<img
|
||||||
class="m-auto max-h-full max-w-full object-contain"
|
class="m-auto max-h-full max-w-full object-contain"
|
||||||
src="/api/oidc/clients/{item.id}/logo"
|
src="/api/oidc/clients/{item.id}/logo"
|
||||||
alt={m.name_logo({name: item.name})}
|
alt={m.name_logo({ name: item.name })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="font-medium">{item.name}</Table.Cell>
|
<Table.Cell class="font-medium">{item.name}</Table.Cell>
|
||||||
@@ -75,8 +77,11 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
aria-label={m.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={m.delete()}
|
<Button
|
||||||
><LucideTrash class="h-3 w-3 text-red-500" /></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>
|
</Table.Cell>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import type { Paginated } from '$lib/types/pagination.type';
|
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
||||||
import type { UserGroupCreate, UserGroupWithUserCount } from '$lib/types/user-group.type';
|
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideMinus } from 'lucide-svelte';
|
import { LucideMinus, UserCog, UserPlus } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import UserGroupForm from './user-group-form.svelte';
|
import UserGroupForm from './user-group-form.svelte';
|
||||||
import UserGroupList from './user-group-list.svelte';
|
import UserGroupList from './user-group-list.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let userGroups = $state(data.userGroups);
|
let userGroups = $state(data.userGroups);
|
||||||
@@ -40,12 +39,17 @@
|
|||||||
<title>{m.user_groups()}</title>
|
<title>{m.user_groups()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.create_user_group()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description>{m.create_a_new_group_that_can_be_assigned_to_users()}</Card.Description>
|
<UserPlus class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.create_user_group()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>{m.create_a_new_group_that_can_be_assigned_to_users()}</Card.Description
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if !expandAddUserGroup}
|
{#if !expandAddUserGroup}
|
||||||
<Button on:click={() => (expandAddUserGroup = true)}>{m.add_group()}</Button>
|
<Button on:click={() => (expandAddUserGroup = true)}>{m.add_group()}</Button>
|
||||||
@@ -63,13 +67,19 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.manage_user_groups()}</Card.Title>
|
<Card.Title>
|
||||||
|
<UserCog class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.manage_user_groups()}
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<UserGroupList {userGroups} requestOptions={userGroupsRequestOptions} />
|
<UserGroupList {userGroups} requestOptions={userGroupsRequestOptions} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { UserCreate } from '$lib/types/user.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideMinus } from 'lucide-svelte';
|
import { LucideMinus, UserPen, UserPlus } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import UserForm from './user-form.svelte';
|
import UserForm from './user-form.svelte';
|
||||||
import UserList from './user-list.svelte';
|
import UserList from './user-list.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let users = $state(data.users);
|
let users = $state(data.users);
|
||||||
@@ -39,12 +39,20 @@
|
|||||||
<title>{m.users()}</title>
|
<title>{m.users()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Card.Title>{m.create_user()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description>{m.add_a_new_user_to_appname({ appName: $appConfigStore.appName })}.</Card.Description>
|
<UserPlus class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.create_user()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description
|
||||||
|
>{m.add_a_new_user_to_appname({
|
||||||
|
appName: $appConfigStore.appName
|
||||||
|
})}.</Card.Description
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if !expandAddUser}
|
{#if !expandAddUser}
|
||||||
<Button on:click={() => (expandAddUser = true)}>{m.add_user()}</Button>
|
<Button on:click={() => (expandAddUser = true)}>{m.add_user()}</Button>
|
||||||
@@ -62,13 +70,19 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.manage_users()}</Card.Title>
|
<Card.Title>
|
||||||
|
<UserPen class="text-primary/80 h-5 w-5" />
|
||||||
|
{m.manage_users()}
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<UserList {users} requestOptions={usersRequestOptions} />
|
<UserList {users} requestOptions={usersRequestOptions} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { LogsIcon } from 'lucide-svelte';
|
||||||
import AuditLogList from './audit-log-list.svelte';
|
import AuditLogList from './audit-log-list.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let { auditLogs } = data;
|
|
||||||
let auditLogsRequestOptions = $state(data.auditLogsRequestOptions);
|
let auditLogsRequestOptions = $state(data.auditLogsRequestOptions);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -12,14 +12,17 @@
|
|||||||
<title>{m.audit_log()}</title>
|
<title>{m.audit_log()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<div>
|
||||||
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{m.audit_log()}</Card.Title>
|
<Card.Title>
|
||||||
<Card.Description class="mt-1"
|
<LogsIcon class="text-primary/80 h-5 w-5" />
|
||||||
>{m.see_your_account_activities_from_the_last_3_months()}</Card.Description
|
{m.audit_log()}
|
||||||
>
|
</Card.Title>
|
||||||
|
<Card.Description>{m.see_your_account_activities_from_the_last_3_months()}</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<AuditLogList auditLogs={data.auditLogs} requestOptions={auditLogsRequestOptions} />
|
<AuditLogList auditLogs={data.auditLogs} requestOptions={auditLogsRequestOptions} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user