includio-cms 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/ROADMAP.md +17 -15
- package/dist/admin/auth-client.d.ts +1165 -5
- package/dist/admin/auth-client.js +4 -1
- package/dist/admin/client/account/sessions-section.svelte +1 -21
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/users/accept-invite-page.svelte +118 -0
- package/dist/admin/client/users/accept-invite-page.svelte.d.ts +4 -0
- package/dist/admin/client/users/create-user-dialog.svelte +157 -0
- package/dist/admin/client/users/create-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/delete-user-dialog.svelte +53 -0
- package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +10 -0
- package/dist/admin/client/users/edit-user-dialog.svelte +127 -0
- package/dist/admin/client/users/edit-user-dialog.svelte.d.ts +16 -0
- package/dist/admin/client/users/invite-user-dialog.svelte +107 -0
- package/dist/admin/client/users/invite-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/lang.d.ts +57 -0
- package/dist/admin/client/users/lang.js +114 -0
- package/dist/admin/client/users/pending-invitations.svelte +145 -0
- package/dist/admin/client/users/pending-invitations.svelte.d.ts +6 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte +141 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte.d.ts +8 -0
- package/dist/admin/client/users/users-page.svelte +262 -0
- package/dist/admin/client/users/users-page.svelte.d.ts +6 -0
- package/dist/admin/components/fields/array-field.svelte +68 -22
- package/dist/admin/components/fields/field-renderer.svelte +25 -2
- package/dist/admin/components/fields/number-field.svelte +1 -1
- package/dist/admin/components/fields/text-field-wrapper.svelte +56 -1
- package/dist/admin/components/fields/text-field.svelte +2 -2
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/nav-main.svelte +15 -1
- package/dist/admin/remote/invite.d.ts +44 -0
- package/dist/admin/remote/invite.js +44 -0
- package/dist/admin/remote/middleware/auth.d.ts +5 -0
- package/dist/admin/remote/middleware/auth.js +7 -0
- package/dist/admin/utils/parseUserAgent.d.ts +5 -0
- package/dist/admin/utils/parseUserAgent.js +26 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/cms.d.ts +1 -1
- package/dist/core/cms.js +1 -1
- package/dist/core/fields/fieldSchemaToTs.js +18 -4
- package/dist/core/server/forms/submissions/operations/create.js +1 -1
- package/dist/email-nodemailer/index.d.ts +1 -0
- package/dist/server/auth.d.ts +8 -8
- package/dist/server/db/schema/auth-schema.d.ts +143 -0
- package/dist/server/db/schema/auth-schema.js +12 -0
- package/dist/sveltekit/server/handle.js +13 -0
- package/dist/types/cms.d.ts +2 -2
- package/dist/types/roles.d.ts +1 -0
- package/dist/types/roles.js +1 -0
- package/dist/updates/0.1.1/index.d.ts +2 -0
- package/dist/updates/0.1.1/index.js +17 -0
- package/dist/updates/0.1.2/index.d.ts +2 -0
- package/dist/updates/0.1.2/index.js +36 -0
- package/dist/updates/index.js +3 -1
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
|
|
11
11
|
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
12
12
|
import Button from '../../../components/ui/button/button.svelte';
|
|
13
|
+
import { parseUserAgent } from '../../utils/parseUserAgent.js';
|
|
13
14
|
|
|
14
15
|
const interfaceLanguage = useInterfaceLanguage();
|
|
15
16
|
|
|
@@ -65,27 +66,6 @@
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function parseUserAgent(ua: string | null | undefined): { browser: string; os: string; isMobile: boolean } {
|
|
69
|
-
if (!ua) return { browser: 'Unknown', os: 'Unknown', isMobile: false };
|
|
70
|
-
|
|
71
|
-
const isMobile = /mobile|android|iphone|ipad/i.test(ua);
|
|
72
|
-
|
|
73
|
-
let browser = 'Unknown';
|
|
74
|
-
if (ua.includes('Firefox')) browser = 'Firefox';
|
|
75
|
-
else if (ua.includes('Edg')) browser = 'Edge';
|
|
76
|
-
else if (ua.includes('Chrome')) browser = 'Chrome';
|
|
77
|
-
else if (ua.includes('Safari')) browser = 'Safari';
|
|
78
|
-
|
|
79
|
-
let os = 'Unknown';
|
|
80
|
-
if (ua.includes('Windows')) os = 'Windows';
|
|
81
|
-
else if (ua.includes('Mac')) os = 'macOS';
|
|
82
|
-
else if (ua.includes('Linux')) os = 'Linux';
|
|
83
|
-
else if (ua.includes('Android')) os = 'Android';
|
|
84
|
-
else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
|
|
85
|
-
|
|
86
|
-
return { browser, os, isMobile };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
69
|
function formatDate(date: Date | string): string {
|
|
90
70
|
return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
|
|
91
71
|
year: 'numeric',
|
|
@@ -7,3 +7,4 @@ export { default as LoginPage } from './login/login-page.svelte';
|
|
|
7
7
|
export { default as MediaPage } from './media/media-page.svelte';
|
|
8
8
|
export { default as FormPage } from './form/form-page.svelte';
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
|
+
export { default as UsersPage } from './users/users-page.svelte';
|
|
@@ -7,3 +7,4 @@ export { default as LoginPage } from './login/login-page.svelte';
|
|
|
7
7
|
export { default as MediaPage } from './media/media-page.svelte';
|
|
8
8
|
export { default as FormPage } from './form/form-page.svelte';
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
|
+
export { default as UsersPage } from './users/users-page.svelte';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../../styles/admin.css';
|
|
3
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
4
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
5
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
6
|
+
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
7
|
+
import { page } from '$app/state';
|
|
8
|
+
import { goto } from '$app/navigation';
|
|
9
|
+
import { usersLang } from './lang.js';
|
|
10
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
11
|
+
|
|
12
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
13
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
14
|
+
|
|
15
|
+
const token = $derived(page.url.searchParams.get('token'));
|
|
16
|
+
|
|
17
|
+
let name = $state('');
|
|
18
|
+
let password = $state('');
|
|
19
|
+
let confirmPassword = $state('');
|
|
20
|
+
let loading = $state(false);
|
|
21
|
+
let error = $state('');
|
|
22
|
+
let success = $state(false);
|
|
23
|
+
|
|
24
|
+
async function handleSubmit(e: Event) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
if (password !== confirmPassword) {
|
|
27
|
+
error = lang.invite.confirmPassword + ' — mismatch';
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (password.length < 6) {
|
|
31
|
+
error = 'Password must be at least 6 characters';
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
loading = true;
|
|
36
|
+
error = '';
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch('/admin/api/accept-invite', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ token, name, password })
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
error = data.error || 'Error';
|
|
49
|
+
loading = false;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
success = true;
|
|
54
|
+
setTimeout(() => goto('/admin/login'), 2000);
|
|
55
|
+
} catch {
|
|
56
|
+
error = 'Network error';
|
|
57
|
+
loading = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
class="relative flex min-h-svh flex-col items-center justify-center overflow-hidden p-6 md:p-10"
|
|
64
|
+
style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);"
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
class="pointer-events-none absolute inset-0 hidden dark:block"
|
|
68
|
+
style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #2D4A77 100%);"
|
|
69
|
+
></div>
|
|
70
|
+
|
|
71
|
+
<div class="relative z-10 w-full max-w-sm">
|
|
72
|
+
<div
|
|
73
|
+
class="overflow-hidden rounded-3xl border border-slate-200/50 bg-white/80 p-8 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/60"
|
|
74
|
+
>
|
|
75
|
+
<div class="mb-6 text-center">
|
|
76
|
+
<div class="mb-2 text-3xl font-bold" style="background: linear-gradient(135deg, #2D4A77, #4975AE); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
|
|
77
|
+
includio
|
|
78
|
+
</div>
|
|
79
|
+
<h1 class="text-lg font-semibold">{lang.invite.acceptTitle}</h1>
|
|
80
|
+
<p class="text-muted-foreground mt-1 text-sm">{lang.invite.acceptDescription}</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{#if success}
|
|
84
|
+
<div class="rounded-lg bg-green-50 p-4 text-center text-sm text-green-700 dark:bg-green-900/20 dark:text-green-400">
|
|
85
|
+
{lang.invite.accepted}
|
|
86
|
+
</div>
|
|
87
|
+
{:else if !token}
|
|
88
|
+
<div class="rounded-lg bg-red-50 p-4 text-center text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
|
|
89
|
+
{lang.invite.invalidToken}
|
|
90
|
+
</div>
|
|
91
|
+
{:else}
|
|
92
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
93
|
+
<div class="space-y-2">
|
|
94
|
+
<Label for="invite-name">{lang.name}</Label>
|
|
95
|
+
<Input id="invite-name" bind:value={name} required class="rounded-xl" />
|
|
96
|
+
</div>
|
|
97
|
+
<div class="space-y-2">
|
|
98
|
+
<Label for="invite-password">{lang.password}</Label>
|
|
99
|
+
<Input id="invite-password" type="password" bind:value={password} required minlength={6} class="rounded-xl" />
|
|
100
|
+
</div>
|
|
101
|
+
<div class="space-y-2">
|
|
102
|
+
<Label for="invite-confirm">{lang.invite.confirmPassword}</Label>
|
|
103
|
+
<Input id="invite-confirm" type="password" bind:value={confirmPassword} required minlength={6} class="rounded-xl" />
|
|
104
|
+
</div>
|
|
105
|
+
{#if error}
|
|
106
|
+
<p class="text-destructive text-sm">{error}</p>
|
|
107
|
+
{/if}
|
|
108
|
+
<Button type="submit" class="w-full rounded-xl" disabled={loading}>
|
|
109
|
+
{#if loading}
|
|
110
|
+
<Loader2 class="mr-2 size-4 animate-spin" />
|
|
111
|
+
{/if}
|
|
112
|
+
{lang.invite.acceptInvite}
|
|
113
|
+
</Button>
|
|
114
|
+
</form>
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Dialog from '../../../components/ui/dialog/index.js';
|
|
3
|
+
import * as Select from '../../../components/ui/select/index.js';
|
|
4
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
5
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
6
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
7
|
+
import EyeIcon from '@tabler/icons-svelte/icons/eye';
|
|
8
|
+
import EyeOffIcon from '@tabler/icons-svelte/icons/eye-off';
|
|
9
|
+
import DiceIcon from '@tabler/icons-svelte/icons/dice-3';
|
|
10
|
+
import { authClient } from '../../auth-client.js';
|
|
11
|
+
import { toast } from 'svelte-sonner';
|
|
12
|
+
import { usersLang } from './lang.js';
|
|
13
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
14
|
+
import type { UserRole } from '../../../types/roles.js';
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
onCreated: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let { open = $bindable(), onOpenChange, onCreated }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
25
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
26
|
+
|
|
27
|
+
let name = $state('');
|
|
28
|
+
let email = $state('');
|
|
29
|
+
let password = $state('');
|
|
30
|
+
let role = $state<UserRole>('user');
|
|
31
|
+
let loading = $state(false);
|
|
32
|
+
let error = $state('');
|
|
33
|
+
let showPassword = $state(false);
|
|
34
|
+
|
|
35
|
+
function generatePassword(length = 20): string {
|
|
36
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*';
|
|
37
|
+
const array = new Uint8Array(length);
|
|
38
|
+
crypto.getRandomValues(array);
|
|
39
|
+
return Array.from(array, (b) => chars[b % chars.length]).join('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function reset() {
|
|
43
|
+
name = '';
|
|
44
|
+
email = '';
|
|
45
|
+
password = '';
|
|
46
|
+
role = 'user';
|
|
47
|
+
error = '';
|
|
48
|
+
showPassword = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleSubmit(e: Event) {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
loading = true;
|
|
54
|
+
error = '';
|
|
55
|
+
|
|
56
|
+
const { error: apiError } = await authClient.admin.createUser({
|
|
57
|
+
email,
|
|
58
|
+
password,
|
|
59
|
+
name,
|
|
60
|
+
role
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (apiError) {
|
|
64
|
+
error = apiError.message || 'Error';
|
|
65
|
+
loading = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toast.success(lang.userCreated);
|
|
70
|
+
loading = false;
|
|
71
|
+
reset();
|
|
72
|
+
onOpenChange(false);
|
|
73
|
+
onCreated();
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<Dialog.Root
|
|
78
|
+
{open}
|
|
79
|
+
onOpenChange={(v) => {
|
|
80
|
+
if (!v) reset();
|
|
81
|
+
onOpenChange(v);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Dialog.Content>
|
|
85
|
+
<Dialog.Header>
|
|
86
|
+
<Dialog.Title>{lang.createUser}</Dialog.Title>
|
|
87
|
+
</Dialog.Header>
|
|
88
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
89
|
+
<div class="space-y-2">
|
|
90
|
+
<Label for="create-name">{lang.name}</Label>
|
|
91
|
+
<Input id="create-name" bind:value={name} required />
|
|
92
|
+
</div>
|
|
93
|
+
<div class="space-y-2">
|
|
94
|
+
<Label for="create-email">{lang.email}</Label>
|
|
95
|
+
<Input id="create-email" type="email" bind:value={email} required />
|
|
96
|
+
</div>
|
|
97
|
+
<div class="space-y-2">
|
|
98
|
+
<Label for="create-password">{lang.password}</Label>
|
|
99
|
+
<div class="flex gap-1">
|
|
100
|
+
<div class="relative flex-1">
|
|
101
|
+
<Input
|
|
102
|
+
id="create-password"
|
|
103
|
+
type={showPassword ? 'text' : 'password'}
|
|
104
|
+
bind:value={password}
|
|
105
|
+
required
|
|
106
|
+
minlength={6}
|
|
107
|
+
class="pr-9"
|
|
108
|
+
/>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
class="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
|
|
112
|
+
onclick={() => (showPassword = !showPassword)}
|
|
113
|
+
>
|
|
114
|
+
{#if showPassword}
|
|
115
|
+
<EyeOffIcon class="size-4" />
|
|
116
|
+
{:else}
|
|
117
|
+
<EyeIcon class="size-4" />
|
|
118
|
+
{/if}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
<Button
|
|
122
|
+
type="button"
|
|
123
|
+
variant="outline"
|
|
124
|
+
size="icon"
|
|
125
|
+
class="shrink-0"
|
|
126
|
+
onclick={() => {
|
|
127
|
+
password = generatePassword();
|
|
128
|
+
showPassword = true;
|
|
129
|
+
}}
|
|
130
|
+
title={lang.generatePassword}
|
|
131
|
+
>
|
|
132
|
+
<DiceIcon class="size-4" />
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="space-y-2">
|
|
137
|
+
<Label>{lang.role}</Label>
|
|
138
|
+
<Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
|
|
139
|
+
<Select.Trigger class="w-full">
|
|
140
|
+
{role === 'admin' ? lang.roleAdmin : lang.roleUser}
|
|
141
|
+
</Select.Trigger>
|
|
142
|
+
<Select.Content>
|
|
143
|
+
<Select.Item value="user">{lang.roleUser}</Select.Item>
|
|
144
|
+
<Select.Item value="admin">{lang.roleAdmin}</Select.Item>
|
|
145
|
+
</Select.Content>
|
|
146
|
+
</Select.Root>
|
|
147
|
+
</div>
|
|
148
|
+
{#if error}
|
|
149
|
+
<p class="text-destructive text-sm">{error}</p>
|
|
150
|
+
{/if}
|
|
151
|
+
<Dialog.Footer>
|
|
152
|
+
<Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
|
|
153
|
+
<Button type="submit" disabled={loading}>{lang.createUser}</Button>
|
|
154
|
+
</Dialog.Footer>
|
|
155
|
+
</form>
|
|
156
|
+
</Dialog.Content>
|
|
157
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onCreated: () => void;
|
|
5
|
+
};
|
|
6
|
+
declare const CreateUserDialog: import("svelte").Component<Props, {}, "open">;
|
|
7
|
+
type CreateUserDialog = ReturnType<typeof CreateUserDialog>;
|
|
8
|
+
export default CreateUserDialog;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
|
|
3
|
+
import { authClient } from '../../auth-client.js';
|
|
4
|
+
import { toast } from 'svelte-sonner';
|
|
5
|
+
import { usersLang } from './lang.js';
|
|
6
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
onDeleted: () => void;
|
|
12
|
+
userId: string | null;
|
|
13
|
+
currentUserId: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let { open = $bindable(), onOpenChange, onDeleted, userId, currentUserId }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
19
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
20
|
+
|
|
21
|
+
async function handleDelete() {
|
|
22
|
+
if (!userId) return;
|
|
23
|
+
|
|
24
|
+
if (userId === currentUserId) {
|
|
25
|
+
toast.error(lang.cannotDeleteSelf);
|
|
26
|
+
onOpenChange(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { error } = await authClient.admin.removeUser({ userId });
|
|
31
|
+
|
|
32
|
+
if (error) {
|
|
33
|
+
toast.error(error.message || 'Error');
|
|
34
|
+
onOpenChange(false);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toast.success(lang.userDeleted);
|
|
39
|
+
onOpenChange(false);
|
|
40
|
+
onDeleted();
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<AlertDialog.Root {open} onOpenChange={(v) => onOpenChange(v)}>
|
|
45
|
+
<AlertDialog.Content>
|
|
46
|
+
<AlertDialog.Title>{lang.deleteConfirmTitle}</AlertDialog.Title>
|
|
47
|
+
<AlertDialog.Description>{lang.deleteConfirmDescription}</AlertDialog.Description>
|
|
48
|
+
<AlertDialog.Footer>
|
|
49
|
+
<AlertDialog.Cancel>{lang.cancel}</AlertDialog.Cancel>
|
|
50
|
+
<AlertDialog.Action onclick={handleDelete}>{lang.deleteUser}</AlertDialog.Action>
|
|
51
|
+
</AlertDialog.Footer>
|
|
52
|
+
</AlertDialog.Content>
|
|
53
|
+
</AlertDialog.Root>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onDeleted: () => void;
|
|
5
|
+
userId: string | null;
|
|
6
|
+
currentUserId: string;
|
|
7
|
+
};
|
|
8
|
+
declare const DeleteUserDialog: import("svelte").Component<Props, {}, "open">;
|
|
9
|
+
type DeleteUserDialog = ReturnType<typeof DeleteUserDialog>;
|
|
10
|
+
export default DeleteUserDialog;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Dialog from '../../../components/ui/dialog/index.js';
|
|
3
|
+
import * as Select from '../../../components/ui/select/index.js';
|
|
4
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
5
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
6
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
7
|
+
import { authClient } from '../../auth-client.js';
|
|
8
|
+
import { toast } from 'svelte-sonner';
|
|
9
|
+
import { usersLang } from './lang.js';
|
|
10
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
11
|
+
import type { UserRole } from '../../../types/roles.js';
|
|
12
|
+
|
|
13
|
+
type UserData = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
role: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onOpenChange: (open: boolean) => void;
|
|
23
|
+
onUpdated: () => void;
|
|
24
|
+
user: UserData | null;
|
|
25
|
+
currentUserId: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let { open = $bindable(), onOpenChange, onUpdated, user, currentUserId }: Props = $props();
|
|
29
|
+
|
|
30
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
31
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
32
|
+
|
|
33
|
+
let name = $state('');
|
|
34
|
+
let email = $state('');
|
|
35
|
+
let role = $state<UserRole>('user');
|
|
36
|
+
let loading = $state(false);
|
|
37
|
+
let error = $state('');
|
|
38
|
+
|
|
39
|
+
const isSelf = $derived(user?.id === currentUserId);
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (user) {
|
|
43
|
+
name = user.name;
|
|
44
|
+
email = user.email;
|
|
45
|
+
role = (user.role as UserRole) || 'user';
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function handleSubmit(e: Event) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
if (!user) return;
|
|
52
|
+
loading = true;
|
|
53
|
+
error = '';
|
|
54
|
+
|
|
55
|
+
// Update name/email
|
|
56
|
+
const { error: updateError } = await authClient.admin.updateUser({
|
|
57
|
+
userId: user.id,
|
|
58
|
+
data: { name, email }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (updateError) {
|
|
62
|
+
error = updateError.message || 'Error';
|
|
63
|
+
loading = false;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update role if changed and not self
|
|
68
|
+
if (role !== user.role && !isSelf) {
|
|
69
|
+
const { error: roleError } = await authClient.admin.setRole({
|
|
70
|
+
userId: user.id,
|
|
71
|
+
role
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (roleError) {
|
|
75
|
+
error = roleError.message || 'Error';
|
|
76
|
+
loading = false;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
toast.success(lang.userUpdated);
|
|
82
|
+
loading = false;
|
|
83
|
+
onOpenChange(false);
|
|
84
|
+
onUpdated();
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<Dialog.Root {open} onOpenChange={(v) => onOpenChange(v)}>
|
|
89
|
+
<Dialog.Content>
|
|
90
|
+
<Dialog.Header>
|
|
91
|
+
<Dialog.Title>{lang.editUser}</Dialog.Title>
|
|
92
|
+
</Dialog.Header>
|
|
93
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
94
|
+
<div class="space-y-2">
|
|
95
|
+
<Label for="edit-name">{lang.name}</Label>
|
|
96
|
+
<Input id="edit-name" bind:value={name} required />
|
|
97
|
+
</div>
|
|
98
|
+
<div class="space-y-2">
|
|
99
|
+
<Label for="edit-email">{lang.email}</Label>
|
|
100
|
+
<Input id="edit-email" type="email" bind:value={email} required />
|
|
101
|
+
</div>
|
|
102
|
+
<div class="space-y-2">
|
|
103
|
+
<Label>{lang.role}</Label>
|
|
104
|
+
{#if isSelf}
|
|
105
|
+
<p class="text-muted-foreground text-sm">{lang.cannotChangeSelfRole}</p>
|
|
106
|
+
{:else}
|
|
107
|
+
<Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
|
|
108
|
+
<Select.Trigger class="w-full">
|
|
109
|
+
{role === 'admin' ? lang.roleAdmin : lang.roleUser}
|
|
110
|
+
</Select.Trigger>
|
|
111
|
+
<Select.Content>
|
|
112
|
+
<Select.Item value="user">{lang.roleUser}</Select.Item>
|
|
113
|
+
<Select.Item value="admin">{lang.roleAdmin}</Select.Item>
|
|
114
|
+
</Select.Content>
|
|
115
|
+
</Select.Root>
|
|
116
|
+
{/if}
|
|
117
|
+
</div>
|
|
118
|
+
{#if error}
|
|
119
|
+
<p class="text-destructive text-sm">{error}</p>
|
|
120
|
+
{/if}
|
|
121
|
+
<Dialog.Footer>
|
|
122
|
+
<Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
|
|
123
|
+
<Button type="submit" disabled={loading}>{lang.save}</Button>
|
|
124
|
+
</Dialog.Footer>
|
|
125
|
+
</form>
|
|
126
|
+
</Dialog.Content>
|
|
127
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type UserData = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
role: string;
|
|
6
|
+
};
|
|
7
|
+
type Props = {
|
|
8
|
+
open: boolean;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
onUpdated: () => void;
|
|
11
|
+
user: UserData | null;
|
|
12
|
+
currentUserId: string;
|
|
13
|
+
};
|
|
14
|
+
declare const EditUserDialog: import("svelte").Component<Props, {}, "open">;
|
|
15
|
+
type EditUserDialog = ReturnType<typeof EditUserDialog>;
|
|
16
|
+
export default EditUserDialog;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Dialog from '../../../components/ui/dialog/index.js';
|
|
3
|
+
import * as Select from '../../../components/ui/select/index.js';
|
|
4
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
5
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
6
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
7
|
+
import { toast } from 'svelte-sonner';
|
|
8
|
+
import { usersLang } from './lang.js';
|
|
9
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
10
|
+
import type { UserRole } from '../../../types/roles.js';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
onInvited: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let { open = $bindable(), onOpenChange, onInvited }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
21
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
22
|
+
|
|
23
|
+
let email = $state('');
|
|
24
|
+
let role = $state<UserRole>('user');
|
|
25
|
+
let loading = $state(false);
|
|
26
|
+
let error = $state('');
|
|
27
|
+
|
|
28
|
+
function reset() {
|
|
29
|
+
email = '';
|
|
30
|
+
role = 'user';
|
|
31
|
+
error = '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handleSubmit(e: Event) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
loading = true;
|
|
37
|
+
error = '';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch('/admin/api/invite', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ email, role })
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
if (res.status === 409) {
|
|
50
|
+
error = lang.invite.emailAlreadyRegistered;
|
|
51
|
+
} else {
|
|
52
|
+
error = data.error || 'Error';
|
|
53
|
+
}
|
|
54
|
+
loading = false;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
toast.success(lang.invite.inviteSent);
|
|
59
|
+
loading = false;
|
|
60
|
+
reset();
|
|
61
|
+
onOpenChange(false);
|
|
62
|
+
onInvited();
|
|
63
|
+
} catch {
|
|
64
|
+
error = 'Network error';
|
|
65
|
+
loading = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<Dialog.Root
|
|
71
|
+
{open}
|
|
72
|
+
onOpenChange={(v) => {
|
|
73
|
+
if (!v) reset();
|
|
74
|
+
onOpenChange(v);
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<Dialog.Content>
|
|
78
|
+
<Dialog.Header>
|
|
79
|
+
<Dialog.Title>{lang.invite.inviteUser}</Dialog.Title>
|
|
80
|
+
</Dialog.Header>
|
|
81
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<Label for="invite-email">{lang.email}</Label>
|
|
84
|
+
<Input id="invite-email" type="email" bind:value={email} required />
|
|
85
|
+
</div>
|
|
86
|
+
<div class="space-y-2">
|
|
87
|
+
<Label>{lang.role}</Label>
|
|
88
|
+
<Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
|
|
89
|
+
<Select.Trigger class="w-full">
|
|
90
|
+
{role === 'admin' ? lang.roleAdmin : lang.roleUser}
|
|
91
|
+
</Select.Trigger>
|
|
92
|
+
<Select.Content>
|
|
93
|
+
<Select.Item value="user">{lang.roleUser}</Select.Item>
|
|
94
|
+
<Select.Item value="admin">{lang.roleAdmin}</Select.Item>
|
|
95
|
+
</Select.Content>
|
|
96
|
+
</Select.Root>
|
|
97
|
+
</div>
|
|
98
|
+
{#if error}
|
|
99
|
+
<p class="text-destructive text-sm">{error}</p>
|
|
100
|
+
{/if}
|
|
101
|
+
<Dialog.Footer>
|
|
102
|
+
<Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
|
|
103
|
+
<Button type="submit" disabled={loading}>{lang.invite.sendInvite}</Button>
|
|
104
|
+
</Dialog.Footer>
|
|
105
|
+
</form>
|
|
106
|
+
</Dialog.Content>
|
|
107
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onInvited: () => void;
|
|
5
|
+
};
|
|
6
|
+
declare const InviteUserDialog: import("svelte").Component<Props, {}, "open">;
|
|
7
|
+
type InviteUserDialog = ReturnType<typeof InviteUserDialog>;
|
|
8
|
+
export default InviteUserDialog;
|