@makolabs/ripple 1.2.10 → 1.2.12
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/user-management/adapters/UserManagement.remote.d.ts +0 -57
- package/dist/user-management/adapters/UserManagement.remote.js +81 -237
- package/dist/user-management/adapters/index.d.ts +2 -1
- package/dist/user-management/adapters/index.js +4 -4
- package/dist/user-management/user-management.d.ts +2 -1
- package/package.json +1 -1
|
@@ -1,75 +1,18 @@
|
|
|
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
1
|
import type { User } from '../user-management.js';
|
|
26
2
|
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
3
|
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
4
|
export declare const createUser: import("@sveltejs/kit").RemoteCommand<Partial<User>, Promise<User>>;
|
|
39
|
-
/**
|
|
40
|
-
* Update an existing user
|
|
41
|
-
*/
|
|
42
5
|
export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
|
|
43
6
|
userId: string;
|
|
44
7
|
userData: Partial<User>;
|
|
45
8
|
}, Promise<User>>;
|
|
46
|
-
/**
|
|
47
|
-
* Delete a single user
|
|
48
|
-
*/
|
|
49
9
|
export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
|
|
50
|
-
/**
|
|
51
|
-
* Delete multiple users
|
|
52
|
-
*/
|
|
53
10
|
export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
|
|
54
|
-
/**
|
|
55
|
-
* Get permissions for specific users (batched)
|
|
56
|
-
*
|
|
57
|
-
* Uses 'sub' (userId) parameter in admin API calls.
|
|
58
|
-
* Batches multiple permission requests to avoid n+1 problem.
|
|
59
|
-
*/
|
|
60
11
|
export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, Promise<string[]>>;
|
|
61
|
-
/**
|
|
62
|
-
* Update permissions for a specific user
|
|
63
|
-
*
|
|
64
|
-
* Uses 'sub' (userId) parameter in admin API calls
|
|
65
|
-
*/
|
|
66
12
|
export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
|
|
67
13
|
userId: string;
|
|
68
14
|
permissions: string[];
|
|
69
15
|
}, Promise<void>>;
|
|
70
|
-
/**
|
|
71
|
-
* Generate new API key for a user with optional old key revocation
|
|
72
|
-
*/
|
|
73
16
|
export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
|
|
74
17
|
userId: string;
|
|
75
18
|
permissions: string[];
|
|
@@ -1,46 +1,19 @@
|
|
|
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
1
|
import { query, command } from '$app/server';
|
|
2
|
+
import { getRequestEvent } from '$app/server';
|
|
26
3
|
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
|
|
4
|
+
const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
|
|
38
5
|
const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
|
|
39
|
-
// Organization ID for adding users to organizations
|
|
40
6
|
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
7
|
+
function handleClerkError(error, defaultMessage) {
|
|
8
|
+
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
9
|
+
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
10
|
+
enrichedError.status = error.status;
|
|
11
|
+
enrichedError.details = error.details;
|
|
12
|
+
enrichedError.clerkError = true;
|
|
13
|
+
throw enrichedError;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`${defaultMessage}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
16
|
+
}
|
|
44
17
|
async function makeClerkRequest(endpoint, options = {}) {
|
|
45
18
|
const CLERK_SECRET_KEY = env.CLERK_SECRET_KEY;
|
|
46
19
|
if (!CLERK_SECRET_KEY) {
|
|
@@ -56,8 +29,7 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
56
29
|
});
|
|
57
30
|
if (!response.ok) {
|
|
58
31
|
const errorText = await response.text();
|
|
59
|
-
console.error(
|
|
60
|
-
// Try to parse error details
|
|
32
|
+
console.error(`[Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
61
33
|
let errorDetails;
|
|
62
34
|
try {
|
|
63
35
|
errorDetails = JSON.parse(errorText);
|
|
@@ -65,7 +37,6 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
65
37
|
catch {
|
|
66
38
|
errorDetails = { message: errorText || `${response.status} ${response.statusText}` };
|
|
67
39
|
}
|
|
68
|
-
// Throw error with status and details for better frontend handling
|
|
69
40
|
const error = new Error(errorDetails.message || `Clerk API request failed: ${response.status} ${response.statusText}`);
|
|
70
41
|
error.status = response.status;
|
|
71
42
|
error.details = errorDetails;
|
|
@@ -73,9 +44,6 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
73
44
|
}
|
|
74
45
|
return response.json();
|
|
75
46
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Helper: Make authenticated request to Admin API
|
|
78
|
-
*/
|
|
79
47
|
async function makeAdminRequest(endpoint, options = {}) {
|
|
80
48
|
const ADMIN_API_KEY = env.ADMIN_API_KEY;
|
|
81
49
|
const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
|
|
@@ -98,60 +66,39 @@ async function makeAdminRequest(endpoint, options = {}) {
|
|
|
98
66
|
});
|
|
99
67
|
if (!response.ok) {
|
|
100
68
|
const errorText = await response.text();
|
|
101
|
-
console.error(
|
|
69
|
+
console.error(`[Admin API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
102
70
|
throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
103
71
|
}
|
|
104
72
|
return response.json();
|
|
105
73
|
}
|
|
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
74
|
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
75
|
const filteredPermissions = PERMISSION_PREFIX
|
|
117
76
|
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
118
77
|
: permissions;
|
|
119
78
|
if (filteredPermissions.length === 0) {
|
|
120
|
-
console.log(`⚠️ [createUserPermissions] No ${PERMISSION_PREFIX || ''} permissions provided, skipping`);
|
|
121
79
|
return null;
|
|
122
80
|
}
|
|
123
|
-
|
|
81
|
+
return await makeAdminRequest('/admin/keys', {
|
|
124
82
|
method: 'POST',
|
|
125
83
|
body: JSON.stringify({
|
|
126
84
|
client_id: clientId,
|
|
127
|
-
sub: userId,
|
|
85
|
+
sub: userId,
|
|
128
86
|
scopes: filteredPermissions
|
|
129
87
|
})
|
|
130
88
|
});
|
|
131
|
-
console.log(`✅ [createUserPermissions] Admin key created successfully for user ${userId}`);
|
|
132
|
-
return createData;
|
|
133
89
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Get users with pagination and sorting
|
|
136
|
-
*
|
|
137
|
-
* Transforms adapter options to Clerk API format
|
|
138
|
-
*/
|
|
139
90
|
export const getUsers = query('unchecked', async (options) => {
|
|
140
|
-
console.log('🔍 [getUsers] Fetching users with options:', options);
|
|
141
91
|
try {
|
|
142
|
-
// Transform adapter options to Clerk API format
|
|
143
92
|
const limit = options.pageSize;
|
|
144
93
|
const offset = (options.page - 1) * options.pageSize;
|
|
145
|
-
// Build orderBy string
|
|
146
94
|
let orderBy = '';
|
|
147
95
|
if (options.sortBy) {
|
|
148
96
|
const prefix = options.sortOrder === 'desc' ? '-' : '';
|
|
149
97
|
orderBy = `${prefix}${options.sortBy}`;
|
|
150
98
|
}
|
|
151
99
|
else {
|
|
152
|
-
orderBy = '-created_at';
|
|
100
|
+
orderBy = '-created_at';
|
|
153
101
|
}
|
|
154
|
-
// Build Clerk API parameters
|
|
155
102
|
const params = new URLSearchParams({
|
|
156
103
|
limit: limit.toString(),
|
|
157
104
|
offset: offset.toString(),
|
|
@@ -159,91 +106,66 @@ export const getUsers = query('unchecked', async (options) => {
|
|
|
159
106
|
});
|
|
160
107
|
if (options.query)
|
|
161
108
|
params.append('query', options.query);
|
|
162
|
-
// Fetch users and count in parallel
|
|
163
109
|
const [usersData, countData] = await Promise.all([
|
|
164
110
|
makeClerkRequest(`/users?${params}`),
|
|
165
111
|
makeClerkRequest(`/users/count?${params}`)
|
|
166
112
|
]);
|
|
167
|
-
console.log(`✅ [getUsers] Fetched ${usersData.length} users, total: ${countData.total_count}`);
|
|
168
113
|
return {
|
|
169
114
|
users: usersData,
|
|
170
115
|
totalUsers: countData.total_count || 0
|
|
171
116
|
};
|
|
172
117
|
}
|
|
173
118
|
catch (error) {
|
|
174
|
-
console.error('
|
|
119
|
+
console.error('[getUsers] Error:', error);
|
|
175
120
|
throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
176
121
|
}
|
|
177
122
|
});
|
|
178
|
-
/**
|
|
179
|
-
* Create a new user
|
|
180
|
-
*
|
|
181
|
-
* Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
|
|
182
|
-
*/
|
|
183
123
|
export const createUser = command('unchecked', async (userData) => {
|
|
184
|
-
// Transform User to Clerk API format
|
|
185
124
|
const emailAddress = userData.email_addresses?.[0]?.email_address || '';
|
|
186
125
|
if (!emailAddress) {
|
|
187
126
|
throw new Error('Email address is required');
|
|
188
127
|
}
|
|
189
|
-
console.log('📝 [createUser] Creating new user:', emailAddress);
|
|
190
128
|
try {
|
|
191
|
-
// Extract permissions from userData
|
|
192
129
|
const { permissions, ...userDataOnly } = userData;
|
|
193
|
-
// Transform userData to match Clerk Backend API format (snake_case)
|
|
194
130
|
const clerkUserData = {
|
|
195
131
|
first_name: userDataOnly.first_name || '',
|
|
196
132
|
last_name: userDataOnly.last_name || '',
|
|
197
|
-
email_address: [emailAddress],
|
|
133
|
+
email_address: [emailAddress],
|
|
198
134
|
...(userDataOnly.username && { username: userDataOnly.username }),
|
|
199
135
|
...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
|
|
200
136
|
};
|
|
201
|
-
// Step 1: Create user in Clerk
|
|
202
|
-
console.log('📝 [createUser] Creating user in Clerk...');
|
|
203
137
|
let result = await makeClerkRequest('/users', {
|
|
204
138
|
method: 'POST',
|
|
205
139
|
body: JSON.stringify(clerkUserData)
|
|
206
140
|
});
|
|
207
|
-
// Step 1.5: Add user to organization if configured
|
|
208
141
|
if (ORGANIZATION_ID) {
|
|
209
142
|
try {
|
|
210
|
-
console.log(`🏢 [createUser] Adding user ${result.id} to organization ${ORGANIZATION_ID}...`);
|
|
211
143
|
await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
|
|
212
144
|
method: 'POST',
|
|
213
145
|
body: JSON.stringify({
|
|
214
146
|
user_id: result.id,
|
|
215
|
-
role: 'org:member'
|
|
147
|
+
role: 'org:member'
|
|
216
148
|
})
|
|
217
149
|
});
|
|
218
|
-
console.log(`✅ [createUser] User added to organization successfully`);
|
|
219
150
|
}
|
|
220
151
|
catch (orgError) {
|
|
221
|
-
console.error(
|
|
222
|
-
// Clean up: delete the user since they can't be added to the org
|
|
152
|
+
console.error('[createUser] Failed to add user to organization:', orgError);
|
|
223
153
|
try {
|
|
224
154
|
await makeClerkRequest(`/users/${result.id}`, {
|
|
225
155
|
method: 'DELETE'
|
|
226
156
|
});
|
|
227
|
-
console.log(`🗑️ [createUser] User ${result.id} deleted due to org membership failure`);
|
|
228
157
|
}
|
|
229
158
|
catch (deleteError) {
|
|
230
|
-
console.error(
|
|
159
|
+
console.error('[createUser] Failed to delete user after org membership failure:', deleteError);
|
|
231
160
|
}
|
|
232
161
|
throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
|
|
233
162
|
}
|
|
234
163
|
}
|
|
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
164
|
if (permissions && permissions.length > 0) {
|
|
240
165
|
try {
|
|
241
|
-
console.log(`🔑 [createUser] Creating permissions for new user: ${result.id}`);
|
|
242
166
|
const adminKeyResult = await createUserPermissions(result.id, permissions);
|
|
243
|
-
// Extract the API key from the admin service response
|
|
244
167
|
const apiKey = adminKeyResult?.data?.key;
|
|
245
168
|
if (adminKeyResult && apiKey) {
|
|
246
|
-
// Update user's private metadata with the API key
|
|
247
169
|
const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
|
|
248
170
|
method: 'PATCH',
|
|
249
171
|
body: JSON.stringify({
|
|
@@ -253,60 +175,34 @@ export const createUser = command('unchecked', async (userData) => {
|
|
|
253
175
|
}
|
|
254
176
|
})
|
|
255
177
|
});
|
|
256
|
-
// Update the result with the updated user data
|
|
257
178
|
result = updatedUser;
|
|
258
|
-
console.log(`✅ [createUser] API key stored in user's private metadata`);
|
|
259
179
|
}
|
|
260
|
-
else {
|
|
261
|
-
console.warn(`⚠️ [createUser] No API key found in admin key result`);
|
|
262
|
-
}
|
|
263
|
-
// Add permission info to result
|
|
264
180
|
result.adminKey = adminKeyResult;
|
|
265
181
|
result.permissionsAssigned = permissions;
|
|
266
|
-
console.log(`✅ [createUser] User ${result.id} created with permissions successfully`);
|
|
267
182
|
}
|
|
268
183
|
catch (permissionError) {
|
|
269
|
-
console.error(
|
|
270
|
-
// Don't fail the entire operation, but warn
|
|
184
|
+
console.error('[createUser] Failed to assign permissions:', permissionError);
|
|
271
185
|
result.warning = 'User created but permissions assignment failed';
|
|
272
186
|
result.permissionError =
|
|
273
187
|
permissionError instanceof Error ? permissionError.message : String(permissionError);
|
|
274
188
|
}
|
|
275
189
|
}
|
|
276
|
-
else {
|
|
277
|
-
console.log(`⚠️ [createUser] User ${result.id} created without permissions`);
|
|
278
|
-
}
|
|
279
190
|
return result;
|
|
280
191
|
}
|
|
281
192
|
catch (error) {
|
|
282
|
-
console.error('
|
|
283
|
-
|
|
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'}`);
|
|
193
|
+
console.error('[createUser] Error:', error);
|
|
194
|
+
handleClerkError(error, 'Failed to create user');
|
|
292
195
|
}
|
|
293
196
|
});
|
|
294
|
-
/**
|
|
295
|
-
* Update an existing user
|
|
296
|
-
*/
|
|
297
197
|
export const updateUser = command('unchecked', async (options) => {
|
|
298
198
|
const { userId, userData } = options;
|
|
299
|
-
console.log(`📝 [updateUser] Updating user ${userId}`);
|
|
300
199
|
try {
|
|
301
|
-
// Transform User to Clerk API format
|
|
302
|
-
// Only include fields that Clerk accepts
|
|
303
200
|
const updateData = {};
|
|
304
201
|
if (userData.first_name !== undefined)
|
|
305
202
|
updateData.first_name = userData.first_name;
|
|
306
203
|
if (userData.last_name !== undefined)
|
|
307
204
|
updateData.last_name = userData.last_name;
|
|
308
205
|
if (userData.username !== undefined && userData.username !== '') {
|
|
309
|
-
// Only include username if it's not empty (prevents "username already exists" error)
|
|
310
206
|
updateData.username = userData.username;
|
|
311
207
|
}
|
|
312
208
|
if (userData.private_metadata !== undefined) {
|
|
@@ -316,79 +212,49 @@ export const updateUser = command('unchecked', async (options) => {
|
|
|
316
212
|
method: 'PATCH',
|
|
317
213
|
body: JSON.stringify(updateData)
|
|
318
214
|
});
|
|
319
|
-
// If permissions changed, update them separately
|
|
320
215
|
if (userData.permissions !== undefined) {
|
|
321
216
|
try {
|
|
322
217
|
await updateUserPermissions({ userId, permissions: userData.permissions });
|
|
323
218
|
}
|
|
324
219
|
catch (permError) {
|
|
325
|
-
console.error(
|
|
326
|
-
// Don't fail the entire operation
|
|
220
|
+
console.error('[updateUser] Failed to update permissions:', permError);
|
|
327
221
|
}
|
|
328
222
|
}
|
|
329
|
-
console.log(`✅ [updateUser] User ${userId} updated successfully`);
|
|
330
223
|
return result;
|
|
331
224
|
}
|
|
332
225
|
catch (error) {
|
|
333
|
-
console.error('
|
|
334
|
-
|
|
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'}`);
|
|
226
|
+
console.error('[updateUser] Error:', error);
|
|
227
|
+
handleClerkError(error, 'Failed to update user');
|
|
343
228
|
}
|
|
344
229
|
});
|
|
345
|
-
/**
|
|
346
|
-
* Delete a single user
|
|
347
|
-
*/
|
|
348
230
|
export const deleteUser = command('unchecked', async (userId) => {
|
|
349
|
-
console.log(`🗑️ [deleteUser] Deleting user ${userId}`);
|
|
350
231
|
try {
|
|
351
232
|
await makeClerkRequest(`/users/${userId}`, {
|
|
352
233
|
method: 'DELETE'
|
|
353
234
|
});
|
|
354
|
-
console.log(`✅ [deleteUser] User ${userId} deleted successfully`);
|
|
355
235
|
}
|
|
356
236
|
catch (error) {
|
|
357
|
-
console.error('
|
|
237
|
+
console.error('[deleteUser] Error:', error);
|
|
358
238
|
throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
359
239
|
}
|
|
360
240
|
});
|
|
361
|
-
/**
|
|
362
|
-
* Delete multiple users
|
|
363
|
-
*/
|
|
364
241
|
export const deleteUsers = command('unchecked', async (userIds) => {
|
|
365
|
-
console.log(`🗑️ [deleteUsers] Deleting ${userIds.length} users`);
|
|
366
242
|
try {
|
|
367
243
|
await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
|
|
368
244
|
method: 'DELETE'
|
|
369
245
|
})));
|
|
370
|
-
console.log(`✅ [deleteUsers] ${userIds.length} users deleted successfully`);
|
|
371
246
|
}
|
|
372
247
|
catch (error) {
|
|
373
|
-
console.error('
|
|
248
|
+
console.error('[deleteUsers] Error:', error);
|
|
374
249
|
throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
375
250
|
}
|
|
376
251
|
});
|
|
377
|
-
/**
|
|
378
|
-
* Helper: Fetch permissions for a single user
|
|
379
|
-
*/
|
|
380
252
|
async function fetchUserPermissions(userId) {
|
|
381
|
-
console.log(`🔍 [fetchUserPermissions] Fetching permissions for user: ${userId}`);
|
|
382
|
-
// Try direct lookup first using client_id and sub (userId)
|
|
383
253
|
try {
|
|
384
254
|
const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
385
|
-
console.log(`✅ [fetchUserPermissions] Direct lookup successful for user ${userId}`);
|
|
386
|
-
// Filter the response to only include active keys
|
|
387
255
|
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
388
256
|
userData.data.data = userData.data.data.filter((key) => key.status === 'active');
|
|
389
|
-
console.log(`🔍 [fetchUserPermissions] Filtered to ${userData.data.data.length} active key(s)`);
|
|
390
257
|
}
|
|
391
|
-
// Extract scopes from the response
|
|
392
258
|
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
393
259
|
return userData.data.data.flatMap((key) => key.scopes || []);
|
|
394
260
|
}
|
|
@@ -398,48 +264,28 @@ async function fetchUserPermissions(userId) {
|
|
|
398
264
|
return [];
|
|
399
265
|
}
|
|
400
266
|
catch {
|
|
401
|
-
console.log(`❌ [fetchUserPermissions] Direct lookup failed, trying search by sub field`);
|
|
402
|
-
// If direct lookup fails with 404, search all keys by sub field
|
|
403
267
|
try {
|
|
404
268
|
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
405
|
-
console.log(`🔍 [fetchUserPermissions] Searching through ${allKeysData.data?.data?.length || 0} keys`);
|
|
406
|
-
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
407
|
-
// Match by sub (userId) and client_id
|
|
408
269
|
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
409
270
|
if (userKey) {
|
|
410
|
-
console.log(`✅ [fetchUserPermissions] Found active user key by sub field`);
|
|
411
271
|
return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
|
|
412
272
|
}
|
|
413
|
-
|
|
414
|
-
console.log(`❌ [fetchUserPermissions] No user found, returning empty permissions`);
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
273
|
+
return [];
|
|
417
274
|
}
|
|
418
275
|
catch (searchError) {
|
|
419
|
-
console.error('
|
|
276
|
+
console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
|
|
420
277
|
throw new Error('Failed to fetch user permissions');
|
|
421
278
|
}
|
|
422
279
|
}
|
|
423
280
|
}
|
|
424
|
-
/**
|
|
425
|
-
* Get permissions for specific users (batched)
|
|
426
|
-
*
|
|
427
|
-
* Uses 'sub' (userId) parameter in admin API calls.
|
|
428
|
-
* Batches multiple permission requests to avoid n+1 problem.
|
|
429
|
-
*/
|
|
430
281
|
export const getUserPermissions = query.batch('unchecked', async (userIds) => {
|
|
431
|
-
console.log(`🔍 [getUserPermissions] Batch fetching permissions for ${userIds.length} users`);
|
|
432
282
|
try {
|
|
433
|
-
// Fetch all permissions in parallel
|
|
434
283
|
const permissionPromises = userIds.map((userId) => fetchUserPermissions(userId));
|
|
435
284
|
const permissionsResults = await Promise.all(permissionPromises);
|
|
436
|
-
// Create a lookup map for O(1) access
|
|
437
285
|
const lookup = new Map();
|
|
438
286
|
userIds.forEach((userId, index) => {
|
|
439
287
|
lookup.set(userId, permissionsResults[index]);
|
|
440
288
|
});
|
|
441
|
-
console.log(`✅ [getUserPermissions] Batch fetch completed for ${userIds.length} users`);
|
|
442
|
-
// Return a function that SvelteKit will call for each individual request
|
|
443
289
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
444
290
|
return (userId, _index) => {
|
|
445
291
|
const permissions = lookup.get(userId) || [];
|
|
@@ -447,120 +293,122 @@ export const getUserPermissions = query.batch('unchecked', async (userIds) => {
|
|
|
447
293
|
};
|
|
448
294
|
}
|
|
449
295
|
catch (error) {
|
|
450
|
-
console.error('
|
|
451
|
-
// Return a function that throws for all requests
|
|
296
|
+
console.error('[getUserPermissions] Batch error:', error);
|
|
452
297
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
453
298
|
return (_userId, _index) => {
|
|
454
299
|
throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
455
300
|
};
|
|
456
301
|
}
|
|
457
302
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
303
|
+
async function refreshTokenIfSelfUpdate(userId) {
|
|
304
|
+
try {
|
|
305
|
+
const event = getRequestEvent();
|
|
306
|
+
if (!event?.locals)
|
|
307
|
+
return;
|
|
308
|
+
// Try to get current user - this is app-specific, so we check if the method exists
|
|
309
|
+
const getAuth = event.locals.auth;
|
|
310
|
+
if (!getAuth)
|
|
311
|
+
return;
|
|
312
|
+
const currentUser = getAuth();
|
|
313
|
+
const isSelfUpdate = currentUser?.userId === userId;
|
|
314
|
+
if (isSelfUpdate) {
|
|
315
|
+
try {
|
|
316
|
+
// Try to dynamically import token manager - this is app-specific
|
|
317
|
+
// Use Function constructor to avoid TypeScript checking the import path
|
|
318
|
+
const importPath = '$lib/server/auth-hooks/token-manager.server';
|
|
319
|
+
const tokenManager = await new Function('path', 'return import(path)')(importPath).catch(() => null);
|
|
320
|
+
if (tokenManager?.clearTokenCookies &&
|
|
321
|
+
tokenManager?.getAccessToken &&
|
|
322
|
+
tokenManager?.setClientAccessibleToken) {
|
|
323
|
+
tokenManager.clearTokenCookies(event.cookies);
|
|
324
|
+
const newToken = await tokenManager.getAccessToken(event.cookies, event, true);
|
|
325
|
+
tokenManager.setClientAccessibleToken(event.cookies, newToken);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Token refresh not available in this app, skip silently
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Token refresh not available in this app, skip silently
|
|
335
|
+
}
|
|
336
|
+
}
|
|
463
337
|
export const updateUserPermissions = command('unchecked', async (options) => {
|
|
464
338
|
const { userId, permissions } = options;
|
|
465
|
-
console.log(`🔑 [updateUserPermissions] Updating permissions for user ${userId}`);
|
|
466
339
|
try {
|
|
467
|
-
let adminKeyId = userId;
|
|
468
|
-
// Try direct update first
|
|
340
|
+
let adminKeyId = userId;
|
|
469
341
|
try {
|
|
470
342
|
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
471
343
|
method: 'PUT',
|
|
472
344
|
body: JSON.stringify({ scopes: permissions })
|
|
473
345
|
});
|
|
474
|
-
|
|
346
|
+
await refreshTokenIfSelfUpdate(userId);
|
|
475
347
|
return;
|
|
476
348
|
}
|
|
477
349
|
catch {
|
|
478
|
-
// If direct update fails, try to find the correct adminKeyId
|
|
479
|
-
console.log(`⚠️ [updateUserPermissions] Direct update failed, searching for key ID...`);
|
|
480
350
|
try {
|
|
481
351
|
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
482
|
-
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
483
|
-
// Match by sub (userId) and client_id
|
|
484
352
|
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
485
353
|
if (userKey) {
|
|
486
|
-
// Use the found key ID for update
|
|
487
354
|
adminKeyId = userKey.id;
|
|
488
355
|
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
489
356
|
method: 'PUT',
|
|
490
357
|
body: JSON.stringify({ scopes: permissions })
|
|
491
358
|
});
|
|
492
|
-
|
|
359
|
+
await refreshTokenIfSelfUpdate(userId);
|
|
493
360
|
return;
|
|
494
361
|
}
|
|
495
362
|
else {
|
|
496
|
-
// User doesn't exist, create new admin key
|
|
497
|
-
console.log(`📝 [updateUserPermissions] Creating new admin key...`);
|
|
498
363
|
await createUserPermissions(userId, permissions);
|
|
499
|
-
|
|
364
|
+
await refreshTokenIfSelfUpdate(userId);
|
|
500
365
|
return;
|
|
501
366
|
}
|
|
502
367
|
}
|
|
503
368
|
catch (searchError) {
|
|
504
|
-
console.error('
|
|
369
|
+
console.error('[updateUserPermissions] Error during permission update:', searchError);
|
|
505
370
|
throw new Error('Failed to update permissions');
|
|
506
371
|
}
|
|
507
372
|
}
|
|
508
373
|
}
|
|
509
374
|
catch (error) {
|
|
510
|
-
console.error('
|
|
375
|
+
console.error('[updateUserPermissions] Error:', error);
|
|
511
376
|
throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
512
377
|
}
|
|
513
378
|
});
|
|
514
|
-
/**
|
|
515
|
-
* Generate new API key for a user with optional old key revocation
|
|
516
|
-
*/
|
|
517
379
|
export const generateApiKey = command('unchecked', async (options) => {
|
|
518
|
-
console.log(`🔑 [generateApiKey] Generating new API key for user ${options.userId}`);
|
|
519
380
|
try {
|
|
520
|
-
// Filter permissions by prefix if configured
|
|
521
381
|
const filteredPermissions = PERMISSION_PREFIX
|
|
522
382
|
? options.permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
523
383
|
: options.permissions;
|
|
524
384
|
if (filteredPermissions.length === 0) {
|
|
525
385
|
throw new Error(`At least one ${PERMISSION_PREFIX || ''} permission is required to generate an API key`);
|
|
526
386
|
}
|
|
527
|
-
// If revokeOld is true, find and revoke the old key first
|
|
528
387
|
let oldKeyId = null;
|
|
529
388
|
if (options.revokeOld) {
|
|
530
389
|
try {
|
|
531
|
-
console.log(`🔍 [generateApiKey] Looking for existing active key to revoke`);
|
|
532
390
|
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
533
|
-
if (
|
|
534
|
-
console.warn('⚠️ [generateApiKey] Unexpected response structure from /admin/keys');
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
// Find the ACTIVE key for this user (ignore already revoked keys)
|
|
391
|
+
if (allKeysData?.data?.data && Array.isArray(allKeysData.data.data)) {
|
|
538
392
|
const userKey = allKeysData.data.data.find((key) => key.sub === options.userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
539
393
|
if (userKey) {
|
|
540
394
|
oldKeyId = userKey.id;
|
|
541
|
-
console.log(`📌 [generateApiKey] Found existing active key: ${oldKeyId}`);
|
|
542
395
|
}
|
|
543
396
|
}
|
|
544
397
|
}
|
|
545
398
|
catch (e) {
|
|
546
|
-
console.warn('
|
|
547
|
-
// Continue anyway - not critical
|
|
399
|
+
console.warn('[generateApiKey] Could not fetch existing key for revocation:', e);
|
|
548
400
|
}
|
|
549
401
|
}
|
|
550
|
-
// Create new admin key
|
|
551
402
|
const createData = await createUserPermissions(options.userId, filteredPermissions);
|
|
552
403
|
if (!createData) {
|
|
553
404
|
throw new Error('Failed to create admin key');
|
|
554
405
|
}
|
|
555
406
|
const newApiKey = createData?.data?.key;
|
|
556
407
|
if (!newApiKey) {
|
|
557
|
-
console.error('
|
|
408
|
+
console.error('[generateApiKey] No API key in response:', createData);
|
|
558
409
|
throw new Error('Failed to generate API key - no key in response');
|
|
559
410
|
}
|
|
560
|
-
console.log(`✅ [generateApiKey] New API key generated successfully`);
|
|
561
|
-
// Update user's Clerk profile with the new API key
|
|
562
411
|
try {
|
|
563
|
-
// First, get the current user data to preserve existing private_metadata
|
|
564
412
|
const currentUser = await makeClerkRequest(`/users/${options.userId}`);
|
|
565
413
|
await makeClerkRequest(`/users/${options.userId}`, {
|
|
566
414
|
method: 'PATCH',
|
|
@@ -571,24 +419,20 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
571
419
|
}
|
|
572
420
|
})
|
|
573
421
|
});
|
|
574
|
-
console.log(`✅ [generateApiKey] API key stored in user's Clerk profile`);
|
|
575
422
|
}
|
|
576
423
|
catch (clerkError) {
|
|
577
|
-
console.error('
|
|
578
|
-
console.warn('
|
|
424
|
+
console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
|
|
425
|
+
console.warn('[generateApiKey] Key generated but could not update Clerk profile');
|
|
579
426
|
}
|
|
580
|
-
// Revoke old key if it exists
|
|
581
427
|
if (oldKeyId) {
|
|
582
428
|
try {
|
|
583
|
-
console.log(`🗑️ [generateApiKey] Revoking old key: ${oldKeyId}`);
|
|
584
429
|
await makeAdminRequest(`/admin/keys/${oldKeyId}`, {
|
|
585
430
|
method: 'DELETE'
|
|
586
431
|
});
|
|
587
|
-
console.log(`✅ [generateApiKey] Old key revoked successfully`);
|
|
588
432
|
}
|
|
589
433
|
catch (revokeError) {
|
|
590
|
-
console.error('
|
|
591
|
-
console.warn('
|
|
434
|
+
console.error('[generateApiKey] Failed to revoke old key:', revokeError);
|
|
435
|
+
console.warn('[generateApiKey] New key generated but could not revoke old key');
|
|
592
436
|
}
|
|
593
437
|
}
|
|
594
438
|
return {
|
|
@@ -600,7 +444,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
600
444
|
};
|
|
601
445
|
}
|
|
602
446
|
catch (error) {
|
|
603
|
-
console.error('
|
|
447
|
+
console.error('[generateApiKey] Error:', error);
|
|
604
448
|
throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
605
449
|
}
|
|
606
450
|
});
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
8
|
*/
|
|
9
9
|
export type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
10
|
-
export * from './
|
|
10
|
+
export * from './UserManagement.remote.js';
|
|
11
|
+
export * as MockUserManagement from './mockUserManagement.js';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
8
|
*/
|
|
9
|
-
// Export
|
|
10
|
-
export * from './
|
|
11
|
-
//
|
|
12
|
-
|
|
9
|
+
// Export default remote functions implementation
|
|
10
|
+
export * from './UserManagement.remote.js';
|
|
11
|
+
// Export mock adapter functions for testing/storybook (with different names to avoid conflicts)
|
|
12
|
+
export * as MockUserManagement from './mockUserManagement.js';
|
|
@@ -110,6 +110,7 @@ export interface UserManagementProps {
|
|
|
110
110
|
/**
|
|
111
111
|
* Adapter module containing remote functions or async functions
|
|
112
112
|
* Should be imported from a .remote.ts file
|
|
113
|
+
* If not provided, uses the default adapter from @makolabs/ripple
|
|
113
114
|
*
|
|
114
115
|
* Example:
|
|
115
116
|
* ```ts
|
|
@@ -117,7 +118,7 @@ export interface UserManagementProps {
|
|
|
117
118
|
* <UserManagement adapter={adapter} roles={roles} />
|
|
118
119
|
* ```
|
|
119
120
|
*/
|
|
120
|
-
adapter
|
|
121
|
+
adapter?: UserManagementAdapter;
|
|
121
122
|
/**
|
|
122
123
|
* Available roles for user assignment
|
|
123
124
|
*/
|