@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.
@@ -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
- users = [],
11
- totalUsers = 0,
12
- loading = false,
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
- if (mode === 'create' && onCreateUser) {
66
- await onCreateUser(user);
67
- } else if (mode === 'edit' && onUpdateUser) {
68
- await onUpdateUser(user.id, user);
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
- if (onDeleteUser) {
78
- await onDeleteUser(userId);
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' && onDeleteUsers) {
93
- await onDeleteUsers(userIds);
94
- selectedUsers.clear();
95
- selectedUsers = new SvelteSet(selectedUsers);
96
- bulkAction = '';
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" disabled={!onCreateUser}>
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
- {onPageChange}
160
- {onPageSizeChange}
161
- {onSort}
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
- users = [],
11
- totalUsers = 0,
12
- loading = false,
13
- currentPage = 1,
14
- pageSize = 10,
15
- roles = [],
16
- permissions = [],
17
- onPageChange = () => {},
18
- onPageSizeChange = () => {},
19
- onSort,
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
- interface Props extends UserManagementProps {
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
+ }
@@ -0,0 +1,7 @@
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
+ export {};
@@ -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
- users: User[];
76
- totalUsers: number;
77
- loading?: boolean;
78
- currentPage?: number;
79
- pageSize?: number;
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.4",
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",