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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
2
|
+
export declare const usersLang: Record<InterfaceLanguage, {
|
|
3
|
+
title: string;
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
role: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
actions: string;
|
|
9
|
+
search: string;
|
|
10
|
+
createUser: string;
|
|
11
|
+
editUser: string;
|
|
12
|
+
deleteUser: string;
|
|
13
|
+
save: string;
|
|
14
|
+
cancel: string;
|
|
15
|
+
password: string;
|
|
16
|
+
roleAdmin: string;
|
|
17
|
+
roleUser: string;
|
|
18
|
+
deleteConfirmTitle: string;
|
|
19
|
+
deleteConfirmDescription: string;
|
|
20
|
+
userCreated: string;
|
|
21
|
+
userUpdated: string;
|
|
22
|
+
userDeleted: string;
|
|
23
|
+
cannotDeleteSelf: string;
|
|
24
|
+
cannotChangeSelfRole: string;
|
|
25
|
+
noResults: string;
|
|
26
|
+
generatePassword: string;
|
|
27
|
+
sessions: {
|
|
28
|
+
title: string;
|
|
29
|
+
noSessions: string;
|
|
30
|
+
revoke: string;
|
|
31
|
+
revokeAll: string;
|
|
32
|
+
sessionRevoked: string;
|
|
33
|
+
allSessionsRevoked: string;
|
|
34
|
+
lastActive: string;
|
|
35
|
+
close: string;
|
|
36
|
+
};
|
|
37
|
+
invite: {
|
|
38
|
+
inviteUser: string;
|
|
39
|
+
sendInvite: string;
|
|
40
|
+
inviteSent: string;
|
|
41
|
+
pendingInvitations: string;
|
|
42
|
+
noPending: string;
|
|
43
|
+
expires: string;
|
|
44
|
+
cancelInvite: string;
|
|
45
|
+
inviteCancelled: string;
|
|
46
|
+
emailAlreadyRegistered: string;
|
|
47
|
+
acceptTitle: string;
|
|
48
|
+
acceptDescription: string;
|
|
49
|
+
confirmPassword: string;
|
|
50
|
+
acceptInvite: string;
|
|
51
|
+
accepted: string;
|
|
52
|
+
invalidToken: string;
|
|
53
|
+
emailConfigRequired: string;
|
|
54
|
+
resend: string;
|
|
55
|
+
resent: string;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const usersLang = {
|
|
2
|
+
en: {
|
|
3
|
+
title: 'Users',
|
|
4
|
+
name: 'Name',
|
|
5
|
+
email: 'Email',
|
|
6
|
+
role: 'Role',
|
|
7
|
+
createdAt: 'Created At',
|
|
8
|
+
actions: 'Actions',
|
|
9
|
+
search: 'Search users...',
|
|
10
|
+
createUser: 'Create User',
|
|
11
|
+
editUser: 'Edit User',
|
|
12
|
+
deleteUser: 'Delete User',
|
|
13
|
+
save: 'Save',
|
|
14
|
+
cancel: 'Cancel',
|
|
15
|
+
password: 'Password',
|
|
16
|
+
roleAdmin: 'Admin',
|
|
17
|
+
roleUser: 'User',
|
|
18
|
+
deleteConfirmTitle: 'Delete user?',
|
|
19
|
+
deleteConfirmDescription: 'This action cannot be undone. The user will be permanently deleted.',
|
|
20
|
+
userCreated: 'User created',
|
|
21
|
+
userUpdated: 'User updated',
|
|
22
|
+
userDeleted: 'User deleted',
|
|
23
|
+
cannotDeleteSelf: 'You cannot delete your own account',
|
|
24
|
+
cannotChangeSelfRole: 'You cannot change your own role',
|
|
25
|
+
noResults: 'No users found',
|
|
26
|
+
generatePassword: 'Generate password',
|
|
27
|
+
sessions: {
|
|
28
|
+
title: 'User Sessions',
|
|
29
|
+
noSessions: 'No active sessions',
|
|
30
|
+
revoke: 'Revoke',
|
|
31
|
+
revokeAll: 'Revoke all sessions',
|
|
32
|
+
sessionRevoked: 'Session revoked',
|
|
33
|
+
allSessionsRevoked: 'All sessions revoked',
|
|
34
|
+
lastActive: 'Last active',
|
|
35
|
+
close: 'Close'
|
|
36
|
+
},
|
|
37
|
+
invite: {
|
|
38
|
+
inviteUser: 'Invite User',
|
|
39
|
+
sendInvite: 'Send Invite',
|
|
40
|
+
inviteSent: 'Invitation sent',
|
|
41
|
+
pendingInvitations: 'Pending Invitations',
|
|
42
|
+
noPending: 'No pending invitations',
|
|
43
|
+
expires: 'Expires',
|
|
44
|
+
cancelInvite: 'Cancel',
|
|
45
|
+
inviteCancelled: 'Invitation cancelled',
|
|
46
|
+
emailAlreadyRegistered: 'This email is already registered',
|
|
47
|
+
acceptTitle: 'Accept Invitation',
|
|
48
|
+
acceptDescription: 'Set up your account to get started.',
|
|
49
|
+
confirmPassword: 'Confirm password',
|
|
50
|
+
acceptInvite: 'Create Account',
|
|
51
|
+
accepted: 'Account created! Redirecting to login...',
|
|
52
|
+
invalidToken: 'Invalid or expired invitation link',
|
|
53
|
+
emailConfigRequired: 'Email adapter not configured',
|
|
54
|
+
resend: 'Resend',
|
|
55
|
+
resent: 'Invitation resent'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
pl: {
|
|
59
|
+
title: 'Użytkownicy',
|
|
60
|
+
name: 'Imię',
|
|
61
|
+
email: 'E-mail',
|
|
62
|
+
role: 'Rola',
|
|
63
|
+
createdAt: 'Utworzono',
|
|
64
|
+
actions: 'Akcje',
|
|
65
|
+
search: 'Szukaj użytkowników...',
|
|
66
|
+
createUser: 'Utwórz użytkownika',
|
|
67
|
+
editUser: 'Edytuj użytkownika',
|
|
68
|
+
deleteUser: 'Usuń użytkownika',
|
|
69
|
+
save: 'Zapisz',
|
|
70
|
+
cancel: 'Anuluj',
|
|
71
|
+
password: 'Hasło',
|
|
72
|
+
roleAdmin: 'Admin',
|
|
73
|
+
roleUser: 'Użytkownik',
|
|
74
|
+
deleteConfirmTitle: 'Usunąć użytkownika?',
|
|
75
|
+
deleteConfirmDescription: 'Ta akcja jest nieodwracalna. Użytkownik zostanie trwale usunięty.',
|
|
76
|
+
userCreated: 'Użytkownik utworzony',
|
|
77
|
+
userUpdated: 'Użytkownik zaktualizowany',
|
|
78
|
+
userDeleted: 'Użytkownik usunięty',
|
|
79
|
+
cannotDeleteSelf: 'Nie możesz usunąć swojego konta',
|
|
80
|
+
cannotChangeSelfRole: 'Nie możesz zmienić swojej roli',
|
|
81
|
+
noResults: 'Nie znaleziono użytkowników',
|
|
82
|
+
generatePassword: 'Wygeneruj hasło',
|
|
83
|
+
sessions: {
|
|
84
|
+
title: 'Sesje użytkownika',
|
|
85
|
+
noSessions: 'Brak aktywnych sesji',
|
|
86
|
+
revoke: 'Zakończ',
|
|
87
|
+
revokeAll: 'Zakończ wszystkie sesje',
|
|
88
|
+
sessionRevoked: 'Sesja zakończona',
|
|
89
|
+
allSessionsRevoked: 'Wszystkie sesje zakończone',
|
|
90
|
+
lastActive: 'Ostatnio aktywna',
|
|
91
|
+
close: 'Zamknij'
|
|
92
|
+
},
|
|
93
|
+
invite: {
|
|
94
|
+
inviteUser: 'Zaproś użytkownika',
|
|
95
|
+
sendInvite: 'Wyślij zaproszenie',
|
|
96
|
+
inviteSent: 'Zaproszenie wysłane',
|
|
97
|
+
pendingInvitations: 'Oczekujące zaproszenia',
|
|
98
|
+
noPending: 'Brak oczekujących zaproszeń',
|
|
99
|
+
expires: 'Wygasa',
|
|
100
|
+
cancelInvite: 'Anuluj',
|
|
101
|
+
inviteCancelled: 'Zaproszenie anulowane',
|
|
102
|
+
emailAlreadyRegistered: 'Ten adres e-mail jest już zarejestrowany',
|
|
103
|
+
acceptTitle: 'Przyjmij zaproszenie',
|
|
104
|
+
acceptDescription: 'Skonfiguruj swoje konto, aby rozpocząć.',
|
|
105
|
+
confirmPassword: 'Potwierdź hasło',
|
|
106
|
+
acceptInvite: 'Utwórz konto',
|
|
107
|
+
accepted: 'Konto utworzone! Przekierowywanie do logowania...',
|
|
108
|
+
invalidToken: 'Nieprawidłowy lub wygasły link zaproszenia',
|
|
109
|
+
emailConfigRequired: 'Adapter e-mail nie jest skonfigurowany',
|
|
110
|
+
resend: 'Wyślij ponownie',
|
|
111
|
+
resent: 'Zaproszenie wysłane ponownie'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Table from '../../../components/ui/table/index.js';
|
|
3
|
+
import { Badge } from '../../../components/ui/badge/index.js';
|
|
4
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
5
|
+
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
6
|
+
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
7
|
+
import Send from '@tabler/icons-svelte/icons/send';
|
|
8
|
+
import { toast } from 'svelte-sonner';
|
|
9
|
+
import { usersLang } from './lang.js';
|
|
10
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
11
|
+
import { toLocaleCode } from '../../utils/formatDate.js';
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
refreshTrigger?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let { refreshTrigger = 0 }: Props = $props();
|
|
18
|
+
|
|
19
|
+
type Invitation = {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
role: string;
|
|
23
|
+
expiresAt: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
28
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
29
|
+
|
|
30
|
+
let invitations = $state<Invitation[]>([]);
|
|
31
|
+
let loading = $state(true);
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
35
|
+
refreshTrigger;
|
|
36
|
+
loadInvitations();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
async function loadInvitations() {
|
|
40
|
+
loading = true;
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch('/admin/api/invite');
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
invitations = data.invitations ?? [];
|
|
45
|
+
} catch {
|
|
46
|
+
invitations = [];
|
|
47
|
+
}
|
|
48
|
+
loading = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resendInvitation(id: string) {
|
|
52
|
+
const res = await fetch('/admin/api/invite', {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ id })
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (res.ok) {
|
|
59
|
+
toast.success(lang.invite.resent);
|
|
60
|
+
} else {
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
toast.error(data.error || 'Error');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function cancelInvitation(id: string) {
|
|
67
|
+
const res = await fetch('/admin/api/invite', {
|
|
68
|
+
method: 'DELETE',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ id })
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
toast.success(lang.invite.inviteCancelled);
|
|
75
|
+
await loadInvitations();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatDate(date: string): string {
|
|
80
|
+
return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
|
|
81
|
+
year: 'numeric',
|
|
82
|
+
month: 'short',
|
|
83
|
+
day: 'numeric'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<div class="mt-8">
|
|
89
|
+
<h2 class="mb-4 text-lg font-semibold">{lang.invite.pendingInvitations}</h2>
|
|
90
|
+
|
|
91
|
+
{#if loading}
|
|
92
|
+
<div class="flex items-center justify-center py-8">
|
|
93
|
+
<Loader2 class="text-muted-foreground size-6 animate-spin" />
|
|
94
|
+
</div>
|
|
95
|
+
{:else if invitations.length === 0}
|
|
96
|
+
<p class="text-muted-foreground py-4 text-center text-sm">{lang.invite.noPending}</p>
|
|
97
|
+
{:else}
|
|
98
|
+
<div class="overflow-hidden rounded-2xl border">
|
|
99
|
+
<Table.Root>
|
|
100
|
+
<Table.Header>
|
|
101
|
+
<Table.Row>
|
|
102
|
+
<Table.Head>{lang.email}</Table.Head>
|
|
103
|
+
<Table.Head>{lang.role}</Table.Head>
|
|
104
|
+
<Table.Head>{lang.invite.expires}</Table.Head>
|
|
105
|
+
<Table.Head class="text-right">{lang.actions}</Table.Head>
|
|
106
|
+
</Table.Row>
|
|
107
|
+
</Table.Header>
|
|
108
|
+
<Table.Body>
|
|
109
|
+
{#each invitations as inv (inv.id)}
|
|
110
|
+
<Table.Row>
|
|
111
|
+
<Table.Cell>{inv.email}</Table.Cell>
|
|
112
|
+
<Table.Cell>
|
|
113
|
+
<Badge variant={inv.role === 'admin' ? 'default' : 'secondary'}>
|
|
114
|
+
{inv.role === 'admin' ? lang.roleAdmin : lang.roleUser}
|
|
115
|
+
</Badge>
|
|
116
|
+
</Table.Cell>
|
|
117
|
+
<Table.Cell>{formatDate(inv.expiresAt)}</Table.Cell>
|
|
118
|
+
<Table.Cell class="text-right">
|
|
119
|
+
<div class="flex items-center justify-end gap-1">
|
|
120
|
+
<Button
|
|
121
|
+
variant="ghost"
|
|
122
|
+
size="icon"
|
|
123
|
+
class="h-8 w-8"
|
|
124
|
+
title={lang.invite.resend}
|
|
125
|
+
onclick={() => resendInvitation(inv.id)}
|
|
126
|
+
>
|
|
127
|
+
<Send class="size-4" />
|
|
128
|
+
</Button>
|
|
129
|
+
<Button
|
|
130
|
+
variant="ghost"
|
|
131
|
+
size="icon"
|
|
132
|
+
class="text-destructive h-8 w-8"
|
|
133
|
+
onclick={() => cancelInvitation(inv.id)}
|
|
134
|
+
>
|
|
135
|
+
<Trash class="size-4" />
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
</Table.Cell>
|
|
139
|
+
</Table.Row>
|
|
140
|
+
{/each}
|
|
141
|
+
</Table.Body>
|
|
142
|
+
</Table.Root>
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
</div>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Sheet from '../../../components/ui/sheet/index.js';
|
|
3
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
4
|
+
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
5
|
+
import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
|
|
6
|
+
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
7
|
+
import { authClient } from '../../auth-client.js';
|
|
8
|
+
import { toast } from 'svelte-sonner';
|
|
9
|
+
import { parseUserAgent } from '../../utils/parseUserAgent.js';
|
|
10
|
+
import { usersLang } from './lang.js';
|
|
11
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
12
|
+
import { toLocaleCode } from '../../utils/formatDate.js';
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
open: boolean;
|
|
16
|
+
userId: string | null;
|
|
17
|
+
userName: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let { open = $bindable(), userId, userName }: Props = $props();
|
|
21
|
+
|
|
22
|
+
type Session = {
|
|
23
|
+
id: string;
|
|
24
|
+
userAgent?: string | null;
|
|
25
|
+
ipAddress?: string | null;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
token: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
32
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
33
|
+
|
|
34
|
+
let sessions = $state<Session[]>([]);
|
|
35
|
+
let loading = $state(false);
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (open && userId) {
|
|
39
|
+
loadSessions(userId);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function loadSessions(uid: string) {
|
|
44
|
+
loading = true;
|
|
45
|
+
const { data, error } = await authClient.admin.listUserSessions({
|
|
46
|
+
userId: uid
|
|
47
|
+
});
|
|
48
|
+
if (data) {
|
|
49
|
+
sessions = data.sessions as Session[];
|
|
50
|
+
} else {
|
|
51
|
+
sessions = [];
|
|
52
|
+
}
|
|
53
|
+
loading = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function revokeSession(sessionToken: string) {
|
|
57
|
+
const { error } = await authClient.admin.revokeUserSession({
|
|
58
|
+
sessionToken
|
|
59
|
+
});
|
|
60
|
+
if (error) {
|
|
61
|
+
toast.error(error.message || 'Failed to revoke session');
|
|
62
|
+
} else {
|
|
63
|
+
toast.success(lang.sessions.sessionRevoked);
|
|
64
|
+
if (userId) await loadSessions(userId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function revokeAll() {
|
|
69
|
+
if (!userId) return;
|
|
70
|
+
const { error } = await authClient.admin.revokeUserSessions({
|
|
71
|
+
userId
|
|
72
|
+
});
|
|
73
|
+
if (error) {
|
|
74
|
+
toast.error(error.message || 'Failed to revoke sessions');
|
|
75
|
+
} else {
|
|
76
|
+
toast.success(lang.sessions.allSessionsRevoked);
|
|
77
|
+
await loadSessions(userId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatDate(date: Date | string): string {
|
|
82
|
+
return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
|
|
83
|
+
year: 'numeric',
|
|
84
|
+
month: 'short',
|
|
85
|
+
day: 'numeric',
|
|
86
|
+
hour: '2-digit',
|
|
87
|
+
minute: '2-digit'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<Sheet.Root bind:open>
|
|
93
|
+
<Sheet.Content side="right" class="w-[400px] sm:max-w-[400px]">
|
|
94
|
+
<Sheet.Header>
|
|
95
|
+
<Sheet.Title>{lang.sessions.title} — {userName}</Sheet.Title>
|
|
96
|
+
</Sheet.Header>
|
|
97
|
+
<div class="flex-1 overflow-y-auto px-4 pb-4">
|
|
98
|
+
{#if loading}
|
|
99
|
+
<div class="flex items-center justify-center py-8">
|
|
100
|
+
<Loader2 class="text-muted-foreground size-6 animate-spin" />
|
|
101
|
+
</div>
|
|
102
|
+
{:else if sessions.length === 0}
|
|
103
|
+
<p class="text-muted-foreground py-8 text-center text-sm">{lang.sessions.noSessions}</p>
|
|
104
|
+
{:else}
|
|
105
|
+
<div class="space-y-3">
|
|
106
|
+
{#each sessions as session}
|
|
107
|
+
{@const ua = parseUserAgent(session.userAgent)}
|
|
108
|
+
<div class="flex items-center justify-between rounded-lg border p-3">
|
|
109
|
+
<div class="flex items-center gap-3">
|
|
110
|
+
<div class="text-muted-foreground">
|
|
111
|
+
{#if ua.isMobile}
|
|
112
|
+
<DeviceMobile class="size-5" />
|
|
113
|
+
{:else}
|
|
114
|
+
<DeviceDesktop class="size-5" />
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<div class="text-sm font-medium">{ua.browser} on {ua.os}</div>
|
|
119
|
+
<div class="text-muted-foreground text-xs">
|
|
120
|
+
{session.ipAddress || 'Unknown IP'} · {lang.sessions.lastActive}: {formatDate(session.updatedAt)}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<Button variant="ghost" size="sm" class="text-destructive text-xs" onclick={() => revokeSession(session.token)}>
|
|
125
|
+
{lang.sessions.revoke}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
{/each}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{#if sessions.length > 1}
|
|
132
|
+
<div class="mt-4 border-t pt-4">
|
|
133
|
+
<Button variant="ghost" size="sm" class="text-destructive" onclick={revokeAll}>
|
|
134
|
+
{lang.sessions.revokeAll}
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
{/if}
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
</Sheet.Content>
|
|
141
|
+
</Sheet.Root>
|