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