@makolabs/ripple 1.2.4 → 1.2.8
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/dist/elements/pagination/Pagination.svelte +1 -1
- package/dist/user-management/UserManagement.svelte +91 -30
- package/dist/user-management/UserManagementTestWrapper.svelte +22 -36
- package/dist/user-management/UserManagementTestWrapper.svelte.d.ts +7 -1
- package/dist/user-management/UserModal.svelte +0 -18
- package/dist/user-management/UserViewModal.svelte +0 -6
- package/dist/user-management/adapters/UserManagement.remote.d.ts +68 -0
- package/dist/user-management/adapters/UserManagement.remote.js +487 -0
- package/dist/user-management/adapters/index.d.ts +10 -0
- package/dist/user-management/adapters/index.js +12 -0
- package/dist/user-management/adapters/mockUserManagement.d.ts +70 -0
- package/dist/user-management/adapters/mockUserManagement.js +187 -0
- package/dist/user-management/adapters/types.d.ts +24 -0
- package/dist/user-management/adapters/types.js +7 -0
- package/dist/user-management/index.d.ts +2 -0
- package/dist/user-management/user-management.d.ts +42 -15
- package/package.json +2 -1
|
@@ -324,7 +324,7 @@
|
|
|
324
324
|
{#if template === 'full' && showPageNumbers}
|
|
325
325
|
{#if totalPages <= maxVisiblePages}
|
|
326
326
|
<!-- Show all pages if total is less than or equal to maxVisiblePages -->
|
|
327
|
-
{#each Array(totalPages) as page, i (page + i)}
|
|
327
|
+
{#each Array(totalPages) as page, i (page + '-' + i)}
|
|
328
328
|
{@const pageNum = i + 1}
|
|
329
329
|
<button
|
|
330
330
|
onclick={() => goToPage(pageNum)}
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
2
3
|
import { PageHeader, MetricCard, Button, cn } from '../index.js';
|
|
3
4
|
import UserTable from './UserTable.svelte';
|
|
4
5
|
import UserModal from './UserModal.svelte';
|
|
5
6
|
import UserViewModal from './UserViewModal.svelte';
|
|
6
|
-
import type { User, UserManagementProps } from './user-management.js';
|
|
7
|
+
import type { User, UserManagementProps, Role, Permission } from './user-management.js';
|
|
7
8
|
import { SvelteSet } from 'svelte/reactivity';
|
|
9
|
+
import * as UserManagementAdapter from './adapters/UserManagement.remote.js';
|
|
8
10
|
|
|
9
11
|
let {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
currentPage = 1,
|
|
14
|
-
pageSize = 10,
|
|
15
|
-
roles = [],
|
|
16
|
-
permissions = [],
|
|
17
|
-
onPageChange,
|
|
18
|
-
onPageSizeChange,
|
|
19
|
-
onSort,
|
|
20
|
-
onCreateUser,
|
|
21
|
-
onUpdateUser,
|
|
22
|
-
onDeleteUser,
|
|
23
|
-
onDeleteUsers,
|
|
12
|
+
adapter = UserManagementAdapter,
|
|
13
|
+
roles: initialRoles = [],
|
|
14
|
+
permissions: initialPermissions = [],
|
|
24
15
|
class: className
|
|
25
16
|
}: UserManagementProps = $props();
|
|
26
17
|
|
|
18
|
+
// Internal state
|
|
19
|
+
let users = $state<User[]>([]);
|
|
20
|
+
let totalUsers = $state(0);
|
|
21
|
+
let loading = $state(false);
|
|
22
|
+
let currentPage = $state(1);
|
|
23
|
+
let pageSize = $state(10);
|
|
24
|
+
let sortBy = $state<string | null>(null);
|
|
25
|
+
let sortOrder = $state<'asc' | 'desc'>('desc');
|
|
26
|
+
let roles = $state<Role[]>(initialRoles);
|
|
27
|
+
let permissions = $state<Permission[]>(initialPermissions);
|
|
28
|
+
|
|
27
29
|
// Modal states
|
|
28
30
|
let showEditCreateModal = $state(false);
|
|
29
31
|
let showViewModal = $state(false);
|
|
@@ -37,6 +39,45 @@
|
|
|
37
39
|
const hasSelectedUsers = $derived(selectedUsers.size > 0);
|
|
38
40
|
const activeUsers = $derived(users.filter((u) => !!u.id));
|
|
39
41
|
|
|
42
|
+
// Load users from adapter (remote function)
|
|
43
|
+
async function loadUsers() {
|
|
44
|
+
try {
|
|
45
|
+
loading = true;
|
|
46
|
+
const result = await adapter.getUsers({
|
|
47
|
+
page: currentPage,
|
|
48
|
+
pageSize,
|
|
49
|
+
sortBy: sortBy || undefined,
|
|
50
|
+
sortOrder: sortOrder || 'desc'
|
|
51
|
+
});
|
|
52
|
+
users = result.users;
|
|
53
|
+
totalUsers = result.totalUsers;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error loading users:', error);
|
|
56
|
+
} finally {
|
|
57
|
+
loading = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handlers
|
|
62
|
+
async function handlePageChange(page: number) {
|
|
63
|
+
currentPage = page;
|
|
64
|
+
await loadUsers();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handlePageSizeChange(size: number) {
|
|
68
|
+
pageSize = size;
|
|
69
|
+
currentPage = 1; // Reset to first page when changing page size
|
|
70
|
+
await loadUsers();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleSort(state: { column: string | null; direction: 'asc' | 'desc' | null }) {
|
|
74
|
+
if (state.column && state.direction) {
|
|
75
|
+
sortBy = state.column;
|
|
76
|
+
sortOrder = state.direction;
|
|
77
|
+
await loadUsers();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
// Modal handlers
|
|
41
82
|
function openViewModal(user: User) {
|
|
42
83
|
selectedUser = user;
|
|
@@ -62,10 +103,16 @@
|
|
|
62
103
|
|
|
63
104
|
// Save handlers
|
|
64
105
|
async function handleUserSave(user: User, mode: 'create' | 'edit') {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
106
|
+
try {
|
|
107
|
+
if (mode === 'create') {
|
|
108
|
+
await adapter.createUser(user);
|
|
109
|
+
} else {
|
|
110
|
+
await adapter.updateUser({ userId: user.id, userData: user });
|
|
111
|
+
}
|
|
112
|
+
await loadUsers();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error saving user:', error);
|
|
115
|
+
throw error;
|
|
69
116
|
}
|
|
70
117
|
}
|
|
71
118
|
|
|
@@ -74,8 +121,12 @@
|
|
|
74
121
|
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
|
75
122
|
return;
|
|
76
123
|
}
|
|
77
|
-
|
|
78
|
-
await
|
|
124
|
+
try {
|
|
125
|
+
await adapter.deleteUser(userId);
|
|
126
|
+
await loadUsers();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error deleting user:', error);
|
|
129
|
+
throw error;
|
|
79
130
|
}
|
|
80
131
|
}
|
|
81
132
|
|
|
@@ -89,13 +140,23 @@
|
|
|
89
140
|
return;
|
|
90
141
|
}
|
|
91
142
|
|
|
92
|
-
if (bulkAction === 'delete'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
if (bulkAction === 'delete') {
|
|
144
|
+
try {
|
|
145
|
+
await adapter.deleteUsers(userIds);
|
|
146
|
+
selectedUsers.clear();
|
|
147
|
+
bulkAction = '';
|
|
148
|
+
await loadUsers();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Error deleting users:', error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
97
153
|
}
|
|
98
154
|
}
|
|
155
|
+
|
|
156
|
+
// Initialize on mount
|
|
157
|
+
onMount(async () => {
|
|
158
|
+
await loadUsers();
|
|
159
|
+
});
|
|
99
160
|
</script>
|
|
100
161
|
|
|
101
162
|
{#snippet PlusIcon()}
|
|
@@ -112,7 +173,7 @@
|
|
|
112
173
|
layout="horizontal"
|
|
113
174
|
class="mb-6"
|
|
114
175
|
>
|
|
115
|
-
<Button onclick={openCreateModal} color="primary"
|
|
176
|
+
<Button onclick={openCreateModal} color="primary">
|
|
116
177
|
{@render PlusIcon()}
|
|
117
178
|
Add User
|
|
118
179
|
</Button>
|
|
@@ -156,9 +217,9 @@
|
|
|
156
217
|
{currentPage}
|
|
157
218
|
{pageSize}
|
|
158
219
|
{totalUsers}
|
|
159
|
-
{
|
|
160
|
-
{
|
|
161
|
-
{
|
|
220
|
+
onPageChange={handlePageChange}
|
|
221
|
+
onPageSizeChange={handlePageSizeChange}
|
|
222
|
+
onSort={handleSort}
|
|
162
223
|
onView={openViewModal}
|
|
163
224
|
onEdit={openEditModal}
|
|
164
225
|
onDelete={handleDeleteUser}
|
|
@@ -1,47 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
2
3
|
import UserManagement from './UserManagement.svelte';
|
|
3
4
|
import type { UserManagementProps } from './user-management.js';
|
|
5
|
+
import * as mockAdapter from './adapters/mockUserManagement.js';
|
|
6
|
+
import { resetState } from './adapters/mockUserManagement.js';
|
|
7
|
+
import type { User, Role, Permission } from './user-management.js';
|
|
4
8
|
|
|
5
|
-
interface Props extends UserManagementProps {
|
|
9
|
+
interface Props extends Omit<UserManagementProps, 'adapter'> {
|
|
6
10
|
testId?: string;
|
|
11
|
+
initialUsers?: User[];
|
|
12
|
+
roles?: Role[];
|
|
13
|
+
permissions?: Permission[];
|
|
14
|
+
simulateDelay?: boolean;
|
|
15
|
+
delayMs?: number;
|
|
7
16
|
}
|
|
8
17
|
|
|
9
|
-
let {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
onCreateUser,
|
|
21
|
-
onUpdateUser,
|
|
22
|
-
onDeleteUser,
|
|
23
|
-
onDeleteUsers,
|
|
24
|
-
testId,
|
|
25
|
-
...rest
|
|
26
|
-
}: Props = $props();
|
|
18
|
+
let { testId, initialUsers, roles, permissions, simulateDelay, delayMs, ...rest }: Props =
|
|
19
|
+
$props();
|
|
20
|
+
|
|
21
|
+
// Initialize mock adapter with provided data
|
|
22
|
+
onMount(async () => {
|
|
23
|
+
await resetState({
|
|
24
|
+
initialUsers,
|
|
25
|
+
simulateDelay,
|
|
26
|
+
delayMs
|
|
27
|
+
});
|
|
28
|
+
});
|
|
27
29
|
</script>
|
|
28
30
|
|
|
29
31
|
<div data-testid={testId}>
|
|
30
|
-
<UserManagement
|
|
31
|
-
{users}
|
|
32
|
-
{totalUsers}
|
|
33
|
-
{loading}
|
|
34
|
-
{currentPage}
|
|
35
|
-
{pageSize}
|
|
36
|
-
{roles}
|
|
37
|
-
{permissions}
|
|
38
|
-
{onPageChange}
|
|
39
|
-
{onPageSizeChange}
|
|
40
|
-
{onSort}
|
|
41
|
-
{onCreateUser}
|
|
42
|
-
{onUpdateUser}
|
|
43
|
-
{onDeleteUser}
|
|
44
|
-
{onDeleteUsers}
|
|
45
|
-
{...rest}
|
|
46
|
-
/>
|
|
32
|
+
<UserManagement adapter={mockAdapter} {roles} {permissions} {...rest} />
|
|
47
33
|
</div>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { UserManagementProps } from './user-management.js';
|
|
2
|
-
|
|
2
|
+
import type { User, Role, Permission } from './user-management.js';
|
|
3
|
+
interface Props extends Omit<UserManagementProps, 'adapter'> {
|
|
3
4
|
testId?: string;
|
|
5
|
+
initialUsers?: User[];
|
|
6
|
+
roles?: Role[];
|
|
7
|
+
permissions?: Permission[];
|
|
8
|
+
simulateDelay?: boolean;
|
|
9
|
+
delayMs?: number;
|
|
4
10
|
}
|
|
5
11
|
declare const UserManagementTestWrapper: import("svelte").Component<Props, {}, "">;
|
|
6
12
|
type UserManagementTestWrapper = ReturnType<typeof UserManagementTestWrapper>;
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
let formData = $state<Partial<User>>({
|
|
26
26
|
first_name: '',
|
|
27
27
|
last_name: '',
|
|
28
|
-
username: '',
|
|
29
28
|
email_addresses: [{ email_address: '' }],
|
|
30
29
|
phone_numbers: [],
|
|
31
30
|
role: '',
|
|
@@ -38,7 +37,6 @@
|
|
|
38
37
|
formData = {
|
|
39
38
|
first_name: user.first_name || '',
|
|
40
39
|
last_name: user.last_name || '',
|
|
41
|
-
username: user.username || '',
|
|
42
40
|
email_addresses: user.email_addresses || [{ email_address: '' }],
|
|
43
41
|
phone_numbers: user.phone_numbers || [],
|
|
44
42
|
role: user.role || '',
|
|
@@ -49,7 +47,6 @@
|
|
|
49
47
|
formData = {
|
|
50
48
|
first_name: '',
|
|
51
49
|
last_name: '',
|
|
52
|
-
username: '',
|
|
53
50
|
email_addresses: [{ email_address: '' }],
|
|
54
51
|
phone_numbers: [],
|
|
55
52
|
role: '',
|
|
@@ -91,7 +88,6 @@
|
|
|
91
88
|
id: user?.id || '',
|
|
92
89
|
first_name: formData.first_name,
|
|
93
90
|
last_name: formData.last_name,
|
|
94
|
-
username: formData.username,
|
|
95
91
|
email_addresses: formData.email_addresses,
|
|
96
92
|
phone_numbers: formData.phone_numbers,
|
|
97
93
|
role: formData.role,
|
|
@@ -194,20 +190,6 @@
|
|
|
194
190
|
<p class="mt-1 text-xs text-red-500">{formErrors.email}</p>
|
|
195
191
|
{/if}
|
|
196
192
|
</div>
|
|
197
|
-
|
|
198
|
-
<!-- Username (Optional) -->
|
|
199
|
-
<div>
|
|
200
|
-
<label for="username" class="mb-1 block text-sm font-medium text-gray-700">
|
|
201
|
-
Username
|
|
202
|
-
</label>
|
|
203
|
-
<input
|
|
204
|
-
id="username"
|
|
205
|
-
type="text"
|
|
206
|
-
bind:value={formData.username}
|
|
207
|
-
class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
|
|
208
|
-
placeholder="username"
|
|
209
|
-
/>
|
|
210
|
-
</div>
|
|
211
193
|
</div>
|
|
212
194
|
|
|
213
195
|
<!-- Right Column: Permissions & Role -->
|
|
@@ -127,12 +127,6 @@
|
|
|
127
127
|
{getUserDisplayName(user)}
|
|
128
128
|
</p>
|
|
129
129
|
</div>
|
|
130
|
-
{#if user?.username}
|
|
131
|
-
<div>
|
|
132
|
-
<span class="text-xs text-gray-500">Username</span>
|
|
133
|
-
<p class="text-sm">{user.username}</p>
|
|
134
|
-
</div>
|
|
135
|
-
{/if}
|
|
136
130
|
<div>
|
|
137
131
|
<span class="text-xs text-gray-500">User ID</span>
|
|
138
132
|
<p class="font-mono text-xs break-all">{user?.id || 'N/A'}</p>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Remote Functions
|
|
3
|
+
*
|
|
4
|
+
* Complete implementation template for user management remote functions.
|
|
5
|
+
* This follows the same pattern as users.remote.ts and is designed to be
|
|
6
|
+
* used as-is in SvelteKit applications like sharkfin-frontend.
|
|
7
|
+
*
|
|
8
|
+
* Configuration:
|
|
9
|
+
* - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
|
|
10
|
+
* - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
|
|
11
|
+
* - Adjust permission prefix filter via PERMISSION_PREFIX env variable
|
|
12
|
+
*
|
|
13
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
14
|
+
*
|
|
15
|
+
* Usage in your app:
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { UserManagement } from '@makolabs/ripple';
|
|
19
|
+
* import * as adapter from '../UserManagement.remote';
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <UserManagement adapter={adapter} />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import type { User } from '../user-management.js';
|
|
26
|
+
import type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
27
|
+
/**
|
|
28
|
+
* Get users with pagination and sorting
|
|
29
|
+
*
|
|
30
|
+
* Transforms adapter options to Clerk API format
|
|
31
|
+
*/
|
|
32
|
+
export declare const getUsers: import("@sveltejs/kit").RemoteQueryFunction<GetUsersOptions, GetUsersResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Create a new user
|
|
35
|
+
*
|
|
36
|
+
* Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
|
|
37
|
+
*/
|
|
38
|
+
export declare const createUser: import("@sveltejs/kit").RemoteCommand<Partial<User>, Promise<User>>;
|
|
39
|
+
/**
|
|
40
|
+
* Update an existing user
|
|
41
|
+
*/
|
|
42
|
+
export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
|
|
43
|
+
userId: string;
|
|
44
|
+
userData: Partial<User>;
|
|
45
|
+
}, Promise<User>>;
|
|
46
|
+
/**
|
|
47
|
+
* Delete a single user
|
|
48
|
+
*/
|
|
49
|
+
export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
|
|
50
|
+
/**
|
|
51
|
+
* Delete multiple users
|
|
52
|
+
*/
|
|
53
|
+
export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
|
|
54
|
+
/**
|
|
55
|
+
* Get permissions for a specific user
|
|
56
|
+
*
|
|
57
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
58
|
+
*/
|
|
59
|
+
export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, string[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Update permissions for a specific user
|
|
62
|
+
*
|
|
63
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
64
|
+
*/
|
|
65
|
+
export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
|
|
66
|
+
userId: string;
|
|
67
|
+
permissions: string[];
|
|
68
|
+
}, Promise<void>>;
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Remote Functions
|
|
3
|
+
*
|
|
4
|
+
* Complete implementation template for user management remote functions.
|
|
5
|
+
* This follows the same pattern as users.remote.ts and is designed to be
|
|
6
|
+
* used as-is in SvelteKit applications like sharkfin-frontend.
|
|
7
|
+
*
|
|
8
|
+
* Configuration:
|
|
9
|
+
* - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
|
|
10
|
+
* - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
|
|
11
|
+
* - Adjust permission prefix filter via PERMISSION_PREFIX env variable
|
|
12
|
+
*
|
|
13
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
14
|
+
*
|
|
15
|
+
* Usage in your app:
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { UserManagement } from '@makolabs/ripple';
|
|
19
|
+
* import * as adapter from '../UserManagement.remote';
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <UserManagement adapter={adapter} />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { query, command } from '$app/server';
|
|
26
|
+
import { env } from '$env/dynamic/private';
|
|
27
|
+
/**
|
|
28
|
+
* Configuration
|
|
29
|
+
* Override these values or import from your config file
|
|
30
|
+
*/
|
|
31
|
+
// Option 1: Import from your config (recommended)
|
|
32
|
+
// import { ADMIN_CONFIG } from '../../constants/permissions';
|
|
33
|
+
// const CLIENT_ID = ADMIN_CONFIG.CLIENT_ID;
|
|
34
|
+
// Option 2: Use environment variable
|
|
35
|
+
const CLIENT_ID = env.CLIENT_ID || 'sharkfin'; // Default fallback
|
|
36
|
+
// Permission prefix filter - adjust based on your permission structure
|
|
37
|
+
// Example: 'sharkfin:' filters to only sharkfin permissions
|
|
38
|
+
const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
|
|
39
|
+
// Organization ID for adding users to organizations
|
|
40
|
+
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
41
|
+
/**
|
|
42
|
+
* Helper: Make authenticated request to Clerk API
|
|
43
|
+
*/
|
|
44
|
+
async function makeClerkRequest(endpoint, options = {}) {
|
|
45
|
+
const CLERK_SECRET_KEY = env.CLERK_SECRET_KEY;
|
|
46
|
+
if (!CLERK_SECRET_KEY) {
|
|
47
|
+
throw new Error('CLERK_SECRET_KEY environment variable is required');
|
|
48
|
+
}
|
|
49
|
+
const response = await fetch(`https://api.clerk.com/v1${endpoint}`, {
|
|
50
|
+
...options,
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${CLERK_SECRET_KEY}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
...options.headers
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
console.error(`❌ [Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
60
|
+
// Try to parse error details
|
|
61
|
+
let errorDetails;
|
|
62
|
+
try {
|
|
63
|
+
errorDetails = JSON.parse(errorText);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
errorDetails = { message: errorText || `${response.status} ${response.statusText}` };
|
|
67
|
+
}
|
|
68
|
+
// Throw error with status and details for better frontend handling
|
|
69
|
+
const error = new Error(errorDetails.message || `Clerk API request failed: ${response.status} ${response.statusText}`);
|
|
70
|
+
error.status = response.status;
|
|
71
|
+
error.details = errorDetails;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Helper: Make authenticated request to Admin API
|
|
78
|
+
*/
|
|
79
|
+
async function makeAdminRequest(endpoint, options = {}) {
|
|
80
|
+
const ADMIN_API_KEY = env.ADMIN_API_KEY;
|
|
81
|
+
const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
|
|
82
|
+
if (!ADMIN_API_KEY || !PRIVATE_BASE_AUTH_URL) {
|
|
83
|
+
const missing = [];
|
|
84
|
+
if (!ADMIN_API_KEY)
|
|
85
|
+
missing.push('ADMIN_API_KEY');
|
|
86
|
+
if (!PRIVATE_BASE_AUTH_URL)
|
|
87
|
+
missing.push('PRIVATE_BASE_AUTH_URL');
|
|
88
|
+
throw new Error(`Admin API configuration missing: ${missing.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
headers: {
|
|
94
|
+
'X-Admin-API-Key': ADMIN_API_KEY,
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
...options.headers
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
console.error(`❌ [Admin API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
102
|
+
throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
103
|
+
}
|
|
104
|
+
return response.json();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Helper: Create admin key with permissions for a user
|
|
108
|
+
*
|
|
109
|
+
* @param userId - The user ID (used as 'sub' in admin API)
|
|
110
|
+
* @param permissions - Array of permission strings
|
|
111
|
+
* @param clientId - Client ID (defaults to CLIENT_ID config)
|
|
112
|
+
*/
|
|
113
|
+
async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
|
|
114
|
+
console.log(`🔑 [createUserPermissions] Creating admin key for user ${userId}`);
|
|
115
|
+
// Filter permissions by prefix if configured
|
|
116
|
+
const filteredPermissions = PERMISSION_PREFIX
|
|
117
|
+
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
118
|
+
: permissions;
|
|
119
|
+
if (filteredPermissions.length === 0) {
|
|
120
|
+
console.log(`⚠️ [createUserPermissions] No ${PERMISSION_PREFIX || ''} permissions provided, skipping`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const createData = await makeAdminRequest('/admin/keys', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
client_id: clientId,
|
|
127
|
+
sub: userId, // userId is used as 'sub' parameter
|
|
128
|
+
scopes: filteredPermissions
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
console.log(`✅ [createUserPermissions] Admin key created successfully for user ${userId}`);
|
|
132
|
+
return createData;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get users with pagination and sorting
|
|
136
|
+
*
|
|
137
|
+
* Transforms adapter options to Clerk API format
|
|
138
|
+
*/
|
|
139
|
+
export const getUsers = query('unchecked', async (options) => {
|
|
140
|
+
console.log('🔍 [getUsers] Fetching users with options:', options);
|
|
141
|
+
try {
|
|
142
|
+
// Transform adapter options to Clerk API format
|
|
143
|
+
const limit = options.pageSize;
|
|
144
|
+
const offset = (options.page - 1) * options.pageSize;
|
|
145
|
+
// Build orderBy string
|
|
146
|
+
let orderBy = '';
|
|
147
|
+
if (options.sortBy) {
|
|
148
|
+
const prefix = options.sortOrder === 'desc' ? '-' : '';
|
|
149
|
+
orderBy = `${prefix}${options.sortBy}`;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
orderBy = '-created_at'; // Default
|
|
153
|
+
}
|
|
154
|
+
// Build Clerk API parameters
|
|
155
|
+
const params = new URLSearchParams({
|
|
156
|
+
limit: limit.toString(),
|
|
157
|
+
offset: offset.toString(),
|
|
158
|
+
order_by: orderBy
|
|
159
|
+
});
|
|
160
|
+
if (options.query)
|
|
161
|
+
params.append('query', options.query);
|
|
162
|
+
// Fetch users and count in parallel
|
|
163
|
+
const [usersData, countData] = await Promise.all([
|
|
164
|
+
makeClerkRequest(`/users?${params}`),
|
|
165
|
+
makeClerkRequest(`/users/count?${params}`)
|
|
166
|
+
]);
|
|
167
|
+
console.log(`✅ [getUsers] Fetched ${usersData.length} users, total: ${countData.total_count}`);
|
|
168
|
+
return {
|
|
169
|
+
users: usersData,
|
|
170
|
+
totalUsers: countData.total_count || 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error('❌ [getUsers] Error:', error);
|
|
175
|
+
throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* Create a new user
|
|
180
|
+
*
|
|
181
|
+
* Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
|
|
182
|
+
*/
|
|
183
|
+
export const createUser = command('unchecked', async (userData) => {
|
|
184
|
+
// Transform User to Clerk API format
|
|
185
|
+
const emailAddress = userData.email_addresses?.[0]?.email_address || '';
|
|
186
|
+
if (!emailAddress) {
|
|
187
|
+
throw new Error('Email address is required');
|
|
188
|
+
}
|
|
189
|
+
console.log('📝 [createUser] Creating new user:', emailAddress);
|
|
190
|
+
try {
|
|
191
|
+
// Extract permissions from userData
|
|
192
|
+
const { permissions, ...userDataOnly } = userData;
|
|
193
|
+
// Transform userData to match Clerk Backend API format (snake_case)
|
|
194
|
+
const clerkUserData = {
|
|
195
|
+
first_name: userDataOnly.first_name || '',
|
|
196
|
+
last_name: userDataOnly.last_name || '',
|
|
197
|
+
email_address: [emailAddress], // Clerk expects an array
|
|
198
|
+
...(userDataOnly.username && { username: userDataOnly.username }),
|
|
199
|
+
...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
|
|
200
|
+
};
|
|
201
|
+
// Step 1: Create user in Clerk
|
|
202
|
+
console.log('📝 [createUser] Creating user in Clerk...');
|
|
203
|
+
let result = await makeClerkRequest('/users', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: JSON.stringify(clerkUserData)
|
|
206
|
+
});
|
|
207
|
+
// Step 1.5: Add user to organization if configured
|
|
208
|
+
if (ORGANIZATION_ID) {
|
|
209
|
+
try {
|
|
210
|
+
console.log(`🏢 [createUser] Adding user ${result.id} to organization ${ORGANIZATION_ID}...`);
|
|
211
|
+
await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
user_id: result.id,
|
|
215
|
+
role: 'org:member' // Standard Clerk organization role
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
console.log(`✅ [createUser] User added to organization successfully`);
|
|
219
|
+
}
|
|
220
|
+
catch (orgError) {
|
|
221
|
+
console.error(`❌ [createUser] Failed to add user to organization:`, orgError);
|
|
222
|
+
// Clean up: delete the user since they can't be added to the org
|
|
223
|
+
try {
|
|
224
|
+
await makeClerkRequest(`/users/${result.id}`, {
|
|
225
|
+
method: 'DELETE'
|
|
226
|
+
});
|
|
227
|
+
console.log(`🗑️ [createUser] User ${result.id} deleted due to org membership failure`);
|
|
228
|
+
}
|
|
229
|
+
catch (deleteError) {
|
|
230
|
+
console.error(`❌ [createUser] Failed to delete user after org membership failure:`, deleteError);
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.warn('⚠️ [createUser] ALLOWED_ORG_ID not configured, skipping organization membership');
|
|
237
|
+
}
|
|
238
|
+
// Step 2: Create admin key with permissions if permissions are provided
|
|
239
|
+
if (permissions && permissions.length > 0) {
|
|
240
|
+
try {
|
|
241
|
+
console.log(`🔑 [createUser] Creating permissions for new user: ${result.id}`);
|
|
242
|
+
const adminKeyResult = await createUserPermissions(result.id, permissions);
|
|
243
|
+
// Extract the API key from the admin service response
|
|
244
|
+
const apiKey = adminKeyResult?.data?.key;
|
|
245
|
+
if (adminKeyResult && apiKey) {
|
|
246
|
+
// Update user's private metadata with the API key
|
|
247
|
+
const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
private_metadata: {
|
|
251
|
+
...result.private_metadata,
|
|
252
|
+
mako_api_key: apiKey
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
});
|
|
256
|
+
// Update the result with the updated user data
|
|
257
|
+
result = updatedUser;
|
|
258
|
+
console.log(`✅ [createUser] API key stored in user's private metadata`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.warn(`⚠️ [createUser] No API key found in admin key result`);
|
|
262
|
+
}
|
|
263
|
+
// Add permission info to result
|
|
264
|
+
result.adminKey = adminKeyResult;
|
|
265
|
+
result.permissionsAssigned = permissions;
|
|
266
|
+
console.log(`✅ [createUser] User ${result.id} created with permissions successfully`);
|
|
267
|
+
}
|
|
268
|
+
catch (permissionError) {
|
|
269
|
+
console.error(`❌ [createUser] Failed to assign permissions:`, permissionError);
|
|
270
|
+
// Don't fail the entire operation, but warn
|
|
271
|
+
result.warning = 'User created but permissions assignment failed';
|
|
272
|
+
result.permissionError =
|
|
273
|
+
permissionError instanceof Error ? permissionError.message : String(permissionError);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.log(`⚠️ [createUser] User ${result.id} created without permissions`);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error('❌ [createUser] Error:', error);
|
|
283
|
+
// Handle Clerk API errors with detailed information
|
|
284
|
+
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
285
|
+
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
286
|
+
enrichedError.status = error.status;
|
|
287
|
+
enrichedError.details = error.details;
|
|
288
|
+
enrichedError.clerkError = true;
|
|
289
|
+
throw enrichedError;
|
|
290
|
+
}
|
|
291
|
+
throw new Error(`Failed to create user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
/**
|
|
295
|
+
* Update an existing user
|
|
296
|
+
*/
|
|
297
|
+
export const updateUser = command('unchecked', async (options) => {
|
|
298
|
+
const { userId, userData } = options;
|
|
299
|
+
console.log(`📝 [updateUser] Updating user ${userId}`);
|
|
300
|
+
try {
|
|
301
|
+
// Transform User to Clerk API format
|
|
302
|
+
// Only include fields that Clerk accepts
|
|
303
|
+
const updateData = {};
|
|
304
|
+
if (userData.first_name !== undefined)
|
|
305
|
+
updateData.first_name = userData.first_name;
|
|
306
|
+
if (userData.last_name !== undefined)
|
|
307
|
+
updateData.last_name = userData.last_name;
|
|
308
|
+
if (userData.username !== undefined && userData.username !== '') {
|
|
309
|
+
// Only include username if it's not empty (prevents "username already exists" error)
|
|
310
|
+
updateData.username = userData.username;
|
|
311
|
+
}
|
|
312
|
+
if (userData.private_metadata !== undefined) {
|
|
313
|
+
updateData.private_metadata = userData.private_metadata;
|
|
314
|
+
}
|
|
315
|
+
const result = await makeClerkRequest(`/users/${userId}`, {
|
|
316
|
+
method: 'PATCH',
|
|
317
|
+
body: JSON.stringify(updateData)
|
|
318
|
+
});
|
|
319
|
+
// If permissions changed, update them separately
|
|
320
|
+
if (userData.permissions !== undefined) {
|
|
321
|
+
try {
|
|
322
|
+
await updateUserPermissions({ userId, permissions: userData.permissions });
|
|
323
|
+
}
|
|
324
|
+
catch (permError) {
|
|
325
|
+
console.error(`❌ [updateUser] Failed to update permissions:`, permError);
|
|
326
|
+
// Don't fail the entire operation
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log(`✅ [updateUser] User ${userId} updated successfully`);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('❌ [updateUser] Error:', error);
|
|
334
|
+
// Handle Clerk API errors with detailed information
|
|
335
|
+
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
336
|
+
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
337
|
+
enrichedError.status = error.status;
|
|
338
|
+
enrichedError.details = error.details;
|
|
339
|
+
enrichedError.clerkError = true;
|
|
340
|
+
throw enrichedError;
|
|
341
|
+
}
|
|
342
|
+
throw new Error(`Failed to update user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
/**
|
|
346
|
+
* Delete a single user
|
|
347
|
+
*/
|
|
348
|
+
export const deleteUser = command('unchecked', async (userId) => {
|
|
349
|
+
console.log(`🗑️ [deleteUser] Deleting user ${userId}`);
|
|
350
|
+
try {
|
|
351
|
+
await makeClerkRequest(`/users/${userId}`, {
|
|
352
|
+
method: 'DELETE'
|
|
353
|
+
});
|
|
354
|
+
console.log(`✅ [deleteUser] User ${userId} deleted successfully`);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('❌ [deleteUser] Error:', error);
|
|
358
|
+
throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
/**
|
|
362
|
+
* Delete multiple users
|
|
363
|
+
*/
|
|
364
|
+
export const deleteUsers = command('unchecked', async (userIds) => {
|
|
365
|
+
console.log(`🗑️ [deleteUsers] Deleting ${userIds.length} users`);
|
|
366
|
+
try {
|
|
367
|
+
await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
|
|
368
|
+
method: 'DELETE'
|
|
369
|
+
})));
|
|
370
|
+
console.log(`✅ [deleteUsers] ${userIds.length} users deleted successfully`);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
console.error('❌ [deleteUsers] Error:', error);
|
|
374
|
+
throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
/**
|
|
378
|
+
* Get permissions for a specific user
|
|
379
|
+
*
|
|
380
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
381
|
+
*/
|
|
382
|
+
export const getUserPermissions = query('unchecked', async (userId) => {
|
|
383
|
+
console.log(`🔍 [getUserPermissions] Fetching permissions for user: ${userId}`);
|
|
384
|
+
try {
|
|
385
|
+
// Try direct lookup first using client_id and sub (userId)
|
|
386
|
+
try {
|
|
387
|
+
const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
388
|
+
console.log(`✅ [getUserPermissions] Direct lookup successful for user ${userId}`);
|
|
389
|
+
// Filter the response to only include active keys
|
|
390
|
+
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
391
|
+
userData.data.data = userData.data.data.filter((key) => key.status === 'active');
|
|
392
|
+
console.log(`🔍 [getUserPermissions] Filtered to ${userData.data.data.length} active key(s)`);
|
|
393
|
+
}
|
|
394
|
+
// Extract scopes from the response
|
|
395
|
+
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
396
|
+
return userData.data.data.flatMap((key) => key.scopes || []);
|
|
397
|
+
}
|
|
398
|
+
else if (userData?.scopes) {
|
|
399
|
+
return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
console.log(`❌ [getUserPermissions] Direct lookup failed, trying search by sub field`);
|
|
405
|
+
// If direct lookup fails with 404, search all keys by sub field
|
|
406
|
+
try {
|
|
407
|
+
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
408
|
+
console.log(`🔍 [getUserPermissions] Searching through ${allKeysData.data?.data?.length || 0} keys`);
|
|
409
|
+
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
410
|
+
// Match by sub (userId) and client_id
|
|
411
|
+
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
412
|
+
if (userKey) {
|
|
413
|
+
console.log(`✅ [getUserPermissions] Found active user key by sub field`);
|
|
414
|
+
return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log(`❌ [getUserPermissions] No user found, returning empty permissions`);
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (searchError) {
|
|
422
|
+
console.error('❌ [getUserPermissions] Error searching for user by sub:', searchError);
|
|
423
|
+
throw new Error('Failed to fetch user permissions');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
console.error('❌ [getUserPermissions] Error:', error);
|
|
429
|
+
throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* Update permissions for a specific user
|
|
434
|
+
*
|
|
435
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
436
|
+
*/
|
|
437
|
+
export const updateUserPermissions = command('unchecked', async (options) => {
|
|
438
|
+
const { userId, permissions } = options;
|
|
439
|
+
console.log(`🔑 [updateUserPermissions] Updating permissions for user ${userId}`);
|
|
440
|
+
try {
|
|
441
|
+
let adminKeyId = userId; // Use userId as default key ID
|
|
442
|
+
// Try direct update first
|
|
443
|
+
try {
|
|
444
|
+
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
445
|
+
method: 'PUT',
|
|
446
|
+
body: JSON.stringify({ scopes: permissions })
|
|
447
|
+
});
|
|
448
|
+
console.log(`✅ [updateUserPermissions] Permissions updated successfully`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// If direct update fails, try to find the correct adminKeyId
|
|
453
|
+
console.log(`⚠️ [updateUserPermissions] Direct update failed, searching for key ID...`);
|
|
454
|
+
try {
|
|
455
|
+
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
456
|
+
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
457
|
+
// Match by sub (userId) and client_id
|
|
458
|
+
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
459
|
+
if (userKey) {
|
|
460
|
+
// Use the found key ID for update
|
|
461
|
+
adminKeyId = userKey.id;
|
|
462
|
+
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
463
|
+
method: 'PUT',
|
|
464
|
+
body: JSON.stringify({ scopes: permissions })
|
|
465
|
+
});
|
|
466
|
+
console.log(`✅ [updateUserPermissions] Permissions updated successfully (after search)`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// User doesn't exist, create new admin key
|
|
471
|
+
console.log(`📝 [updateUserPermissions] Creating new admin key...`);
|
|
472
|
+
await createUserPermissions(userId, permissions);
|
|
473
|
+
console.log(`✅ [updateUserPermissions] New admin key created successfully`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (searchError) {
|
|
478
|
+
console.error('❌ [updateUserPermissions] Error during permission update:', searchError);
|
|
479
|
+
throw new Error('Failed to update permissions');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error('❌ [updateUserPermissions] Error:', error);
|
|
485
|
+
throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Adapters
|
|
3
|
+
*
|
|
4
|
+
* Adapters are remote function modules (.remote.ts files) that export
|
|
5
|
+
* query/command functions.
|
|
6
|
+
*
|
|
7
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
|
+
*/
|
|
9
|
+
export type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
10
|
+
export * from './mockUserManagement.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Adapters
|
|
3
|
+
*
|
|
4
|
+
* Adapters are remote function modules (.remote.ts files) that export
|
|
5
|
+
* query/command functions.
|
|
6
|
+
*
|
|
7
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
|
+
*/
|
|
9
|
+
// Export mock adapter functions for testing/storybook
|
|
10
|
+
export * from './mockUserManagement.js';
|
|
11
|
+
// Note: UserManagement.remote.ts is a template file showing how to implement
|
|
12
|
+
// remote functions in your SvelteKit app. It is not exported from this library.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock User Management Functions
|
|
3
|
+
*
|
|
4
|
+
* Mock implementation of user management functions for testing and development.
|
|
5
|
+
* These are regular async functions that match the UserManagementAdapter interface.
|
|
6
|
+
* They store data in memory.
|
|
7
|
+
*
|
|
8
|
+
* Note: This file uses .remote.ts extension for naming consistency, but these are
|
|
9
|
+
* NOT actual SvelteKit remote functions. They are regular async functions that
|
|
10
|
+
* work in both test and library contexts.
|
|
11
|
+
*
|
|
12
|
+
* For actual remote functions in your app, see UserManagement.remote.ts template.
|
|
13
|
+
*
|
|
14
|
+
* To set initial data, use the resetState function:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { resetState } from './mockUserManagement.js';
|
|
17
|
+
* await resetState({ initialUsers: [...], simulateDelay: false });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import type { User } from '../user-management.js';
|
|
21
|
+
import type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Reset mock adapter state
|
|
24
|
+
*/
|
|
25
|
+
export declare function resetState(options?: {
|
|
26
|
+
initialUsers?: User[];
|
|
27
|
+
simulateDelay?: boolean;
|
|
28
|
+
delayMs?: number;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get users with pagination and sorting
|
|
32
|
+
* Matches UserManagementAdapter.getUsers signature
|
|
33
|
+
*/
|
|
34
|
+
export declare function getUsers(options: GetUsersOptions): Promise<GetUsersResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Create a new user
|
|
37
|
+
* Matches UserManagementAdapter.createUser signature
|
|
38
|
+
*/
|
|
39
|
+
export declare function createUser(userData: Partial<User>): Promise<User>;
|
|
40
|
+
/**
|
|
41
|
+
* Update an existing user
|
|
42
|
+
* Matches UserManagementAdapter.updateUser signature
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateUser(options: {
|
|
45
|
+
userId: string;
|
|
46
|
+
userData: Partial<User>;
|
|
47
|
+
}): Promise<User>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete a single user
|
|
50
|
+
* Matches UserManagementAdapter.deleteUser signature
|
|
51
|
+
*/
|
|
52
|
+
export declare function deleteUser(userId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Delete multiple users
|
|
55
|
+
* Matches UserManagementAdapter.deleteUsers signature
|
|
56
|
+
*/
|
|
57
|
+
export declare function deleteUsers(userIds: string[]): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get permissions for a specific user
|
|
60
|
+
* Matches UserManagementAdapter.getUserPermissions signature
|
|
61
|
+
*/
|
|
62
|
+
export declare function getUserPermissions(userId: string): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Update permissions for a specific user
|
|
65
|
+
* Matches UserManagementAdapter.updateUserPermissions signature
|
|
66
|
+
*/
|
|
67
|
+
export declare function updateUserPermissions(options: {
|
|
68
|
+
userId: string;
|
|
69
|
+
permissions: string[];
|
|
70
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock User Management Functions
|
|
3
|
+
*
|
|
4
|
+
* Mock implementation of user management functions for testing and development.
|
|
5
|
+
* These are regular async functions that match the UserManagementAdapter interface.
|
|
6
|
+
* They store data in memory.
|
|
7
|
+
*
|
|
8
|
+
* Note: This file uses .remote.ts extension for naming consistency, but these are
|
|
9
|
+
* NOT actual SvelteKit remote functions. They are regular async functions that
|
|
10
|
+
* work in both test and library contexts.
|
|
11
|
+
*
|
|
12
|
+
* For actual remote functions in your app, see UserManagement.remote.ts template.
|
|
13
|
+
*
|
|
14
|
+
* To set initial data, use the resetState function:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { resetState } from './mockUserManagement.js';
|
|
17
|
+
* await resetState({ initialUsers: [...], simulateDelay: false });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
// Internal module-level state
|
|
21
|
+
let mockUsers = [];
|
|
22
|
+
let simulateDelay = false;
|
|
23
|
+
let delayMs = 300;
|
|
24
|
+
async function delay() {
|
|
25
|
+
if (simulateDelay) {
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reset mock adapter state
|
|
31
|
+
*/
|
|
32
|
+
export async function resetState(options = {}) {
|
|
33
|
+
mockUsers = options.initialUsers || [];
|
|
34
|
+
simulateDelay = options.simulateDelay ?? false;
|
|
35
|
+
delayMs = options.delayMs ?? 300;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get users with pagination and sorting
|
|
39
|
+
* Matches UserManagementAdapter.getUsers signature
|
|
40
|
+
*/
|
|
41
|
+
export async function getUsers(options) {
|
|
42
|
+
await delay();
|
|
43
|
+
let filteredUsers = [...mockUsers];
|
|
44
|
+
// Apply search query if provided
|
|
45
|
+
if (options.query) {
|
|
46
|
+
const query = options.query.toLowerCase();
|
|
47
|
+
filteredUsers = filteredUsers.filter((user) => user.first_name?.toLowerCase().includes(query) ||
|
|
48
|
+
user.last_name?.toLowerCase().includes(query) ||
|
|
49
|
+
user.username?.toLowerCase().includes(query) ||
|
|
50
|
+
user.email_addresses?.[0]?.email_address?.toLowerCase().includes(query));
|
|
51
|
+
}
|
|
52
|
+
// Apply sorting
|
|
53
|
+
if (options.sortBy) {
|
|
54
|
+
filteredUsers.sort((a, b) => {
|
|
55
|
+
let aValue = '';
|
|
56
|
+
let bValue = '';
|
|
57
|
+
switch (options.sortBy) {
|
|
58
|
+
case 'first_name':
|
|
59
|
+
aValue = a.first_name || '';
|
|
60
|
+
bValue = b.first_name || '';
|
|
61
|
+
break;
|
|
62
|
+
case 'last_name':
|
|
63
|
+
aValue = a.last_name || '';
|
|
64
|
+
bValue = b.last_name || '';
|
|
65
|
+
break;
|
|
66
|
+
case 'email_address':
|
|
67
|
+
aValue = a.email_addresses?.[0]?.email_address || '';
|
|
68
|
+
bValue = b.email_addresses?.[0]?.email_address || '';
|
|
69
|
+
break;
|
|
70
|
+
case 'created_at':
|
|
71
|
+
aValue = a.created_at || 0;
|
|
72
|
+
bValue = b.created_at || 0;
|
|
73
|
+
break;
|
|
74
|
+
case 'last_sign_in_at':
|
|
75
|
+
aValue = a.last_sign_in_at || 0;
|
|
76
|
+
bValue = b.last_sign_in_at || 0;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
82
|
+
return options.sortOrder === 'desc'
|
|
83
|
+
? bValue.localeCompare(aValue)
|
|
84
|
+
: aValue.localeCompare(bValue);
|
|
85
|
+
}
|
|
86
|
+
else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
87
|
+
return options.sortOrder === 'desc' ? bValue - aValue : aValue - bValue;
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Apply pagination
|
|
93
|
+
const start = (options.page - 1) * options.pageSize;
|
|
94
|
+
const end = start + options.pageSize;
|
|
95
|
+
const paginatedUsers = filteredUsers.slice(start, end);
|
|
96
|
+
return {
|
|
97
|
+
users: paginatedUsers,
|
|
98
|
+
totalUsers: filteredUsers.length
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a new user
|
|
103
|
+
* Matches UserManagementAdapter.createUser signature
|
|
104
|
+
*/
|
|
105
|
+
export async function createUser(userData) {
|
|
106
|
+
await delay();
|
|
107
|
+
const newUser = {
|
|
108
|
+
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
109
|
+
first_name: userData.first_name || '',
|
|
110
|
+
last_name: userData.last_name || '',
|
|
111
|
+
username: userData.username,
|
|
112
|
+
email_addresses: userData.email_addresses || [{ email_address: '' }],
|
|
113
|
+
phone_numbers: userData.phone_numbers || [],
|
|
114
|
+
role: userData.role,
|
|
115
|
+
permissions: userData.permissions || [],
|
|
116
|
+
created_at: Date.now(),
|
|
117
|
+
...userData
|
|
118
|
+
};
|
|
119
|
+
mockUsers.push(newUser);
|
|
120
|
+
return newUser;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Update an existing user
|
|
124
|
+
* Matches UserManagementAdapter.updateUser signature
|
|
125
|
+
*/
|
|
126
|
+
export async function updateUser(options) {
|
|
127
|
+
await delay();
|
|
128
|
+
const { userId, userData } = options;
|
|
129
|
+
const userIndex = mockUsers.findIndex((u) => u.id === userId);
|
|
130
|
+
if (userIndex === -1) {
|
|
131
|
+
throw new Error(`User with ID ${userId} not found`);
|
|
132
|
+
}
|
|
133
|
+
const updatedUser = {
|
|
134
|
+
...mockUsers[userIndex],
|
|
135
|
+
...userData,
|
|
136
|
+
id: userId // Ensure ID doesn't change
|
|
137
|
+
};
|
|
138
|
+
mockUsers[userIndex] = updatedUser;
|
|
139
|
+
return updatedUser;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Delete a single user
|
|
143
|
+
* Matches UserManagementAdapter.deleteUser signature
|
|
144
|
+
*/
|
|
145
|
+
export async function deleteUser(userId) {
|
|
146
|
+
await delay();
|
|
147
|
+
const userIndex = mockUsers.findIndex((u) => u.id === userId);
|
|
148
|
+
if (userIndex === -1) {
|
|
149
|
+
throw new Error(`User with ID ${userId} not found`);
|
|
150
|
+
}
|
|
151
|
+
mockUsers.splice(userIndex, 1);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Delete multiple users
|
|
155
|
+
* Matches UserManagementAdapter.deleteUsers signature
|
|
156
|
+
*/
|
|
157
|
+
export async function deleteUsers(userIds) {
|
|
158
|
+
await delay();
|
|
159
|
+
userIds.forEach((userId) => {
|
|
160
|
+
const userIndex = mockUsers.findIndex((u) => u.id === userId);
|
|
161
|
+
if (userIndex !== -1) {
|
|
162
|
+
mockUsers.splice(userIndex, 1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get permissions for a specific user
|
|
168
|
+
* Matches UserManagementAdapter.getUserPermissions signature
|
|
169
|
+
*/
|
|
170
|
+
export async function getUserPermissions(userId) {
|
|
171
|
+
await delay();
|
|
172
|
+
const user = mockUsers.find((u) => u.id === userId);
|
|
173
|
+
return user?.permissions || [];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Update permissions for a specific user
|
|
177
|
+
* Matches UserManagementAdapter.updateUserPermissions signature
|
|
178
|
+
*/
|
|
179
|
+
export async function updateUserPermissions(options) {
|
|
180
|
+
await delay();
|
|
181
|
+
const { userId, permissions } = options;
|
|
182
|
+
const user = mockUsers.find((u) => u.id === userId);
|
|
183
|
+
if (!user) {
|
|
184
|
+
throw new Error(`User with ID ${userId} not found`);
|
|
185
|
+
}
|
|
186
|
+
user.permissions = permissions;
|
|
187
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Adapter Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for user management adapters.
|
|
5
|
+
* These types are used by both remote function modules and the component.
|
|
6
|
+
*/
|
|
7
|
+
import type { User } from '../user-management.js';
|
|
8
|
+
/**
|
|
9
|
+
* Options for fetching users
|
|
10
|
+
*/
|
|
11
|
+
export interface GetUsersOptions {
|
|
12
|
+
page: number;
|
|
13
|
+
pageSize: number;
|
|
14
|
+
sortBy?: string;
|
|
15
|
+
sortOrder?: 'asc' | 'desc';
|
|
16
|
+
query?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Result of fetching users
|
|
20
|
+
*/
|
|
21
|
+
export interface GetUsersResult {
|
|
22
|
+
users: User[];
|
|
23
|
+
totalUsers: number;
|
|
24
|
+
}
|
|
@@ -7,4 +7,6 @@ export { default as UserTable } from './UserTable.svelte';
|
|
|
7
7
|
export { default as UserModal } from './UserModal.svelte';
|
|
8
8
|
export { default as UserViewModal } from './UserViewModal.svelte';
|
|
9
9
|
export type { User, UserEmail, UserPhone, Permission, Role, UserTableProps, UserModalProps, UserViewModalProps, UserManagementProps, FormErrors } from './user-management.js';
|
|
10
|
+
export type { GetUsersOptions, GetUsersResult } from './adapters/index.js';
|
|
11
|
+
export type { UserManagementAdapter } from './user-management.js';
|
|
10
12
|
export { createUser, getUserDisplayName, getUserInitials } from './user-management.js';
|
|
@@ -71,24 +71,51 @@ export interface UserViewModalProps {
|
|
|
71
71
|
onClose: () => void;
|
|
72
72
|
class?: ClassValue;
|
|
73
73
|
}
|
|
74
|
+
import type { GetUsersOptions, GetUsersResult } from './adapters/types.js';
|
|
75
|
+
/**
|
|
76
|
+
* User Management Adapter Interface
|
|
77
|
+
*
|
|
78
|
+
* Defines the contract for user management adapters.
|
|
79
|
+
* Adapters can be remote function modules (returning RemoteQuery/RemoteCommand)
|
|
80
|
+
* or regular async function modules (returning Promise).
|
|
81
|
+
*
|
|
82
|
+
* Uses PromiseLike to accept both Promise and RemoteQuery/RemoteCommand types.
|
|
83
|
+
*/
|
|
84
|
+
export interface UserManagementAdapter {
|
|
85
|
+
getUsers: (options: GetUsersOptions) => PromiseLike<GetUsersResult>;
|
|
86
|
+
createUser: (userData: Partial<User>) => PromiseLike<User>;
|
|
87
|
+
updateUser: (options: {
|
|
88
|
+
userId: string;
|
|
89
|
+
userData: Partial<User>;
|
|
90
|
+
}) => PromiseLike<User>;
|
|
91
|
+
deleteUser: (userId: string) => PromiseLike<void>;
|
|
92
|
+
deleteUsers: (userIds: string[]) => PromiseLike<void>;
|
|
93
|
+
getUserPermissions: (userId: string) => PromiseLike<string[]>;
|
|
94
|
+
updateUserPermissions: (options: {
|
|
95
|
+
userId: string;
|
|
96
|
+
permissions: string[];
|
|
97
|
+
}) => PromiseLike<void>;
|
|
98
|
+
}
|
|
74
99
|
export interface UserManagementProps {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Adapter module containing remote functions or async functions
|
|
102
|
+
* Should be imported from a .remote.ts file
|
|
103
|
+
*
|
|
104
|
+
* Example:
|
|
105
|
+
* ```ts
|
|
106
|
+
* import * as adapter from './adapter.remote';
|
|
107
|
+
* <UserManagement adapter={adapter} roles={roles} />
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
adapter: UserManagementAdapter;
|
|
111
|
+
/**
|
|
112
|
+
* Available roles for user assignment
|
|
113
|
+
*/
|
|
80
114
|
roles?: Role[];
|
|
115
|
+
/**
|
|
116
|
+
* Available permissions for display (optional)
|
|
117
|
+
*/
|
|
81
118
|
permissions?: Permission[];
|
|
82
|
-
onPageChange: (page: number) => void;
|
|
83
|
-
onPageSizeChange: (size: number) => void;
|
|
84
|
-
onSort?: (state: {
|
|
85
|
-
column: string | null;
|
|
86
|
-
direction: 'asc' | 'desc' | null;
|
|
87
|
-
}) => void;
|
|
88
|
-
onCreateUser?: (userData: Partial<User>) => Promise<void>;
|
|
89
|
-
onUpdateUser?: (userId: string, userData: Partial<User>) => Promise<void>;
|
|
90
|
-
onDeleteUser?: (userId: string) => Promise<void>;
|
|
91
|
-
onDeleteUsers?: (userIds: string[]) => Promise<void>;
|
|
92
119
|
class?: ClassValue;
|
|
93
120
|
}
|
|
94
121
|
export interface FormErrors {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"dependencies": {
|
|
118
118
|
"@friendofsvelte/mermaid": "^0.0.4",
|
|
119
119
|
"@friendofsvelte/state": "^0.0.6-ts",
|
|
120
|
+
"@makolabs/ripple": "^1.2.4",
|
|
120
121
|
"@sveltejs/adapter-static": "^3.0.9",
|
|
121
122
|
"compromise": "^14.14.4",
|
|
122
123
|
"dayjs": "^1.11.13",
|