@makolabs/ripple 1.7.6 → 1.7.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.
- package/dist/funcs/user-management.remote.d.ts +1 -0
- package/dist/funcs/user-management.remote.js +189 -54
- package/dist/index.d.ts +1 -0
- package/dist/layout/table/Table.svelte +70 -58
- package/dist/user-management/UserManagement.svelte +29 -3
- package/dist/user-management/UserModal.svelte +25 -1
- package/package.json +1 -9
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { query, command } from '$app/server';
|
|
2
2
|
import { getRequestEvent } from '$app/server';
|
|
3
3
|
import { env } from '$env/dynamic/private';
|
|
4
|
-
|
|
4
|
+
import { building } from '$app/environment';
|
|
5
|
+
const CLIENT_ID = env.CLIENT_ID;
|
|
5
6
|
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
7
|
+
if (!CLIENT_ID && !building) {
|
|
8
|
+
throw new Error('CLIENT_ID environment variable is required');
|
|
9
|
+
}
|
|
10
|
+
if (!ORGANIZATION_ID && !building) {
|
|
11
|
+
throw new Error('ALLOWED_ORG_ID environment variable is required');
|
|
12
|
+
}
|
|
13
|
+
const isTraceLevel = () => env.LOG_LEVEL?.toLowerCase() === 'trace';
|
|
14
|
+
const log = {
|
|
15
|
+
trace: (tag, ...args) => {
|
|
16
|
+
if (isTraceLevel())
|
|
17
|
+
console.debug(`[TRACE][${tag}]`, ...args);
|
|
18
|
+
},
|
|
19
|
+
info: (tag, ...args) => {
|
|
20
|
+
if (isTraceLevel())
|
|
21
|
+
console.log(`[${tag}]`, ...args);
|
|
22
|
+
},
|
|
23
|
+
warn: (tag, ...args) => {
|
|
24
|
+
console.warn(`[${tag}]`, ...args);
|
|
25
|
+
},
|
|
26
|
+
error: (tag, ...args) => {
|
|
27
|
+
console.error(`[${tag}]`, ...args);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
6
30
|
function handleClerkError(error, defaultMessage) {
|
|
7
31
|
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
8
32
|
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
@@ -20,9 +44,17 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
20
44
|
}
|
|
21
45
|
const method = options.method || 'GET';
|
|
22
46
|
const url = `https://api.clerk.com/v1${endpoint}`;
|
|
23
|
-
|
|
47
|
+
log.info('Clerk API', `${method} ${url}`);
|
|
48
|
+
if (options.body)
|
|
49
|
+
log.info('Clerk API', `Payload: ${options.body}`);
|
|
50
|
+
log.trace('Clerk API', 'Request headers:', {
|
|
51
|
+
Authorization: 'Bearer [REDACTED]',
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
...options.headers
|
|
54
|
+
});
|
|
24
55
|
if (options.body)
|
|
25
|
-
|
|
56
|
+
log.trace('Clerk API', 'Parsed payload:', JSON.parse(options.body));
|
|
57
|
+
const start = performance.now();
|
|
26
58
|
const response = await fetch(url, {
|
|
27
59
|
...options,
|
|
28
60
|
headers: {
|
|
@@ -31,10 +63,12 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
31
63
|
...options.headers
|
|
32
64
|
}
|
|
33
65
|
});
|
|
66
|
+
const elapsed = (performance.now() - start).toFixed(2);
|
|
34
67
|
if (!response.ok) {
|
|
35
68
|
const errorText = await response.text();
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
log.error('Clerk API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
70
|
+
log.error('Clerk API', `Body: ${errorText}`);
|
|
71
|
+
log.trace('Clerk API', 'Response headers:', Object.fromEntries(response.headers.entries()));
|
|
38
72
|
let errorDetails;
|
|
39
73
|
try {
|
|
40
74
|
errorDetails = JSON.parse(errorText);
|
|
@@ -48,8 +82,10 @@ async function makeClerkRequest(endpoint, options = {}) {
|
|
|
48
82
|
throw error;
|
|
49
83
|
}
|
|
50
84
|
const data = await response.json();
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
log.info('Clerk API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
86
|
+
log.info('Clerk API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
|
|
87
|
+
log.trace('Clerk API', 'Full response body:', data);
|
|
88
|
+
log.trace('Clerk API', 'Response headers:', Object.fromEntries(response.headers.entries()));
|
|
53
89
|
// Ensure all data is serializable by converting to plain objects
|
|
54
90
|
return JSON.parse(JSON.stringify(data));
|
|
55
91
|
}
|
|
@@ -66,9 +102,17 @@ async function makeAdminRequest(endpoint, options = {}) {
|
|
|
66
102
|
}
|
|
67
103
|
const method = options.method || 'GET';
|
|
68
104
|
const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
|
|
69
|
-
|
|
105
|
+
log.info('Admin API', `${method} ${url}`);
|
|
106
|
+
if (options.body)
|
|
107
|
+
log.info('Admin API', `Payload: ${options.body}`);
|
|
108
|
+
log.trace('Admin API', 'Request headers:', {
|
|
109
|
+
'X-Admin-API-Key': '[REDACTED]',
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
...options.headers
|
|
112
|
+
});
|
|
70
113
|
if (options.body)
|
|
71
|
-
|
|
114
|
+
log.trace('Admin API', 'Parsed payload:', JSON.parse(options.body));
|
|
115
|
+
const start = performance.now();
|
|
72
116
|
const response = await fetch(url, {
|
|
73
117
|
...options,
|
|
74
118
|
headers: {
|
|
@@ -77,15 +121,19 @@ async function makeAdminRequest(endpoint, options = {}) {
|
|
|
77
121
|
...options.headers
|
|
78
122
|
}
|
|
79
123
|
});
|
|
124
|
+
const elapsed = (performance.now() - start).toFixed(2);
|
|
80
125
|
if (!response.ok) {
|
|
81
126
|
const errorText = await response.text();
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
log.error('Admin API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
128
|
+
log.error('Admin API', `Body: ${errorText}`);
|
|
129
|
+
log.trace('Admin API', 'Response headers:', Object.fromEntries(response.headers.entries()));
|
|
84
130
|
throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
85
131
|
}
|
|
86
132
|
const data = await response.json();
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
log.info('Admin API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
134
|
+
log.info('Admin API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
|
|
135
|
+
log.trace('Admin API', 'Full response body:', data);
|
|
136
|
+
log.trace('Admin API', 'Response headers:', Object.fromEntries(response.headers.entries()));
|
|
89
137
|
// Ensure all data is serializable by converting to plain objects
|
|
90
138
|
return JSON.parse(JSON.stringify(data));
|
|
91
139
|
}
|
|
@@ -96,9 +144,14 @@ async function makeAuthRequest(endpoint, options = {}) {
|
|
|
96
144
|
}
|
|
97
145
|
const method = options.method || 'GET';
|
|
98
146
|
const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
|
|
99
|
-
|
|
147
|
+
log.info('Auth API', `${method} ${url}`);
|
|
100
148
|
if (options.body)
|
|
101
|
-
|
|
149
|
+
log.info('Auth API', `Payload: ${options.body}`);
|
|
150
|
+
log.trace('Auth API', 'Request headers:', {
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
...options.headers
|
|
153
|
+
});
|
|
154
|
+
const start = performance.now();
|
|
102
155
|
const response = await fetch(url, {
|
|
103
156
|
...options,
|
|
104
157
|
headers: {
|
|
@@ -106,6 +159,7 @@ async function makeAuthRequest(endpoint, options = {}) {
|
|
|
106
159
|
...options.headers
|
|
107
160
|
}
|
|
108
161
|
});
|
|
162
|
+
const elapsed = (performance.now() - start).toFixed(2);
|
|
109
163
|
const text = await response.text();
|
|
110
164
|
let data;
|
|
111
165
|
try {
|
|
@@ -115,8 +169,10 @@ async function makeAuthRequest(endpoint, options = {}) {
|
|
|
115
169
|
// Not JSON, treat as plain text error (e.g., "404 page not found")
|
|
116
170
|
data = { error: text, message: text };
|
|
117
171
|
}
|
|
118
|
-
|
|
119
|
-
|
|
172
|
+
log.info('Auth API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
173
|
+
log.info('Auth API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
|
|
174
|
+
log.trace('Auth API', 'Full response body:', data);
|
|
175
|
+
log.trace('Auth API', 'Response headers:', Object.fromEntries(response.headers.entries()));
|
|
120
176
|
return {
|
|
121
177
|
ok: response.ok,
|
|
122
178
|
status: response.status,
|
|
@@ -124,6 +180,7 @@ async function makeAuthRequest(endpoint, options = {}) {
|
|
|
124
180
|
};
|
|
125
181
|
}
|
|
126
182
|
async function verifyApiKeyToken(apiKey) {
|
|
183
|
+
log.trace('verifyApiKeyToken', 'Called with apiKey:', apiKey.slice(0, 8) + '...[REDACTED]');
|
|
127
184
|
try {
|
|
128
185
|
const result = await makeAuthRequest('/auth/token', {
|
|
129
186
|
method: 'POST',
|
|
@@ -131,37 +188,43 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
131
188
|
'X-API-Key': apiKey
|
|
132
189
|
}
|
|
133
190
|
});
|
|
191
|
+
log.trace('verifyApiKeyToken', 'Token exchange result ok:', result.ok);
|
|
134
192
|
if (result.ok && result.data?.data?.access_token) {
|
|
135
193
|
const token = result.data.data.access_token;
|
|
194
|
+
log.trace('verifyApiKeyToken', 'Got access_token, verifying...');
|
|
136
195
|
const verifyResult = await makeAuthRequest('/auth/verify', {
|
|
137
196
|
method: 'GET',
|
|
138
197
|
headers: {
|
|
139
198
|
Authorization: `Bearer ${token}`
|
|
140
199
|
}
|
|
141
200
|
});
|
|
201
|
+
log.trace('verifyApiKeyToken', 'Verify result ok:', verifyResult.ok, 'data:', verifyResult.data);
|
|
142
202
|
if (verifyResult.ok && verifyResult.data?.data) {
|
|
143
203
|
// The API returns "scope" (singular) as a space-separated string, not "scopes" array
|
|
144
204
|
const scopeString = verifyResult.data.data.scope;
|
|
145
205
|
const scopes = scopeString ? scopeString.split(' ').filter(Boolean) : [];
|
|
146
206
|
const sub = verifyResult.data.data.sub;
|
|
207
|
+
const client_id = verifyResult.data.data.client_id;
|
|
208
|
+
log.trace('verifyApiKeyToken', 'Verified. sub:', sub, 'scopes:', scopes, 'client_id:', client_id);
|
|
147
209
|
return {
|
|
148
210
|
valid: true,
|
|
149
211
|
scopes: scopes,
|
|
150
|
-
sub: sub
|
|
212
|
+
sub: sub,
|
|
213
|
+
client_id: client_id
|
|
151
214
|
};
|
|
152
215
|
}
|
|
153
216
|
}
|
|
154
217
|
const errorMsg = result.data?.message ||
|
|
155
218
|
result.data?.error ||
|
|
156
219
|
`API key verification failed with status ${result.status}`;
|
|
157
|
-
|
|
220
|
+
log.warn('verifyApiKeyToken', 'Verification failed:', errorMsg);
|
|
158
221
|
return {
|
|
159
222
|
valid: false,
|
|
160
223
|
error: errorMsg
|
|
161
224
|
};
|
|
162
225
|
}
|
|
163
226
|
catch (error) {
|
|
164
|
-
|
|
227
|
+
log.error('verifyApiKeyToken', 'Exception during verification:', error);
|
|
165
228
|
return {
|
|
166
229
|
valid: false,
|
|
167
230
|
error: error instanceof Error ? error.message : 'Unknown error during verification'
|
|
@@ -169,10 +232,12 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
169
232
|
}
|
|
170
233
|
}
|
|
171
234
|
async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
|
|
235
|
+
log.trace('createUserPermissions', 'email:', email, 'permissions:', permissions, 'clientId:', clientId);
|
|
172
236
|
if (permissions.length === 0) {
|
|
237
|
+
log.trace('createUserPermissions', 'No permissions to create, returning null');
|
|
173
238
|
return null;
|
|
174
239
|
}
|
|
175
|
-
|
|
240
|
+
const result = await makeAdminRequest('/admin/keys', {
|
|
176
241
|
method: 'POST',
|
|
177
242
|
body: JSON.stringify({
|
|
178
243
|
client_id: clientId,
|
|
@@ -180,8 +245,11 @@ async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
|
|
|
180
245
|
scopes: permissions
|
|
181
246
|
})
|
|
182
247
|
});
|
|
248
|
+
log.trace('createUserPermissions', 'Result:', result);
|
|
249
|
+
return result;
|
|
183
250
|
}
|
|
184
251
|
export const getUsers = query('unchecked', async (options) => {
|
|
252
|
+
log.trace('getUsers', 'Called with options:', options);
|
|
185
253
|
try {
|
|
186
254
|
const limit = options.pageSize;
|
|
187
255
|
const offset = (options.page - 1) * options.pageSize;
|
|
@@ -200,12 +268,14 @@ export const getUsers = query('unchecked', async (options) => {
|
|
|
200
268
|
});
|
|
201
269
|
if (options.query)
|
|
202
270
|
params.append('query', options.query);
|
|
271
|
+
log.trace('getUsers', 'Query params:', params.toString());
|
|
203
272
|
const [usersData, countData] = await Promise.all([
|
|
204
273
|
makeClerkRequest(`/users?${params}`),
|
|
205
274
|
makeClerkRequest(`/users/count?${params}`)
|
|
206
275
|
]);
|
|
207
276
|
// Extract users array - Clerk API returns { data: [...] } or array directly
|
|
208
277
|
const users = Array.isArray(usersData) ? usersData : usersData?.data || [];
|
|
278
|
+
log.trace('getUsers', 'Fetched', users.length, 'users, total count:', countData);
|
|
209
279
|
// Ensure all data is serializable by converting to plain objects
|
|
210
280
|
const serializedUsers = JSON.parse(JSON.stringify(users));
|
|
211
281
|
const serializedCountData = JSON.parse(JSON.stringify(countData));
|
|
@@ -216,11 +286,12 @@ export const getUsers = query('unchecked', async (options) => {
|
|
|
216
286
|
}));
|
|
217
287
|
}
|
|
218
288
|
catch (error) {
|
|
219
|
-
|
|
289
|
+
log.error('getUsers', 'Error:', error);
|
|
220
290
|
throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
221
291
|
}
|
|
222
292
|
});
|
|
223
293
|
export const createUser = command('unchecked', async (userData) => {
|
|
294
|
+
log.trace('createUser', 'Called with userData:', { ...userData, private_metadata: '[REDACTED]' });
|
|
224
295
|
const emailAddress = userData.email_addresses?.[0]?.email_address || '';
|
|
225
296
|
if (!emailAddress) {
|
|
226
297
|
throw new Error('Email address is required');
|
|
@@ -234,13 +305,16 @@ export const createUser = command('unchecked', async (userData) => {
|
|
|
234
305
|
...(userDataOnly.username && { username: userDataOnly.username }),
|
|
235
306
|
...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
|
|
236
307
|
};
|
|
308
|
+
log.trace('createUser', 'Creating Clerk user with data:', clerkUserData);
|
|
237
309
|
let result = await makeClerkRequest('/users', {
|
|
238
310
|
method: 'POST',
|
|
239
311
|
body: JSON.stringify(clerkUserData)
|
|
240
312
|
});
|
|
241
313
|
// Ensure result is serializable
|
|
242
314
|
result = JSON.parse(JSON.stringify(result));
|
|
315
|
+
log.trace('createUser', 'Clerk user created, id:', result.id);
|
|
243
316
|
if (ORGANIZATION_ID) {
|
|
317
|
+
log.trace('createUser', 'Adding user to org:', ORGANIZATION_ID);
|
|
244
318
|
try {
|
|
245
319
|
await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
|
|
246
320
|
method: 'POST',
|
|
@@ -249,24 +323,29 @@ export const createUser = command('unchecked', async (userData) => {
|
|
|
249
323
|
role: 'org:member'
|
|
250
324
|
})
|
|
251
325
|
});
|
|
326
|
+
log.trace('createUser', 'User added to org successfully');
|
|
252
327
|
}
|
|
253
328
|
catch (orgError) {
|
|
254
|
-
|
|
329
|
+
log.error('createUser', 'Failed to add user to organization:', orgError);
|
|
330
|
+
log.trace('createUser', 'Rolling back: deleting user', result.id);
|
|
255
331
|
try {
|
|
256
332
|
await makeClerkRequest(`/users/${result.id}`, {
|
|
257
333
|
method: 'DELETE'
|
|
258
334
|
});
|
|
335
|
+
log.trace('createUser', 'Rollback delete successful');
|
|
259
336
|
}
|
|
260
337
|
catch (deleteError) {
|
|
261
|
-
|
|
338
|
+
log.error('createUser', 'Failed to delete user after org membership failure:', deleteError);
|
|
262
339
|
}
|
|
263
340
|
throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
|
|
264
341
|
}
|
|
265
342
|
}
|
|
266
343
|
if (permissions && permissions.length > 0) {
|
|
344
|
+
log.trace('createUser', 'Assigning permissions:', permissions);
|
|
267
345
|
try {
|
|
268
346
|
const adminKeyResult = await createUserPermissions(emailAddress, permissions);
|
|
269
347
|
const apiKey = adminKeyResult?.data?.key;
|
|
348
|
+
log.trace('createUser', 'Admin key created, has key:', !!apiKey);
|
|
270
349
|
if (adminKeyResult && apiKey) {
|
|
271
350
|
const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
|
|
272
351
|
method: 'PATCH',
|
|
@@ -284,22 +363,27 @@ export const createUser = command('unchecked', async (userData) => {
|
|
|
284
363
|
result.permissionsAssigned = permissions;
|
|
285
364
|
}
|
|
286
365
|
catch (permissionError) {
|
|
287
|
-
|
|
366
|
+
log.error('createUser', 'Failed to assign permissions:', permissionError);
|
|
288
367
|
result.warning = 'User created but permissions assignment failed';
|
|
289
368
|
result.permissionError =
|
|
290
369
|
permissionError instanceof Error ? permissionError.message : String(permissionError);
|
|
291
370
|
}
|
|
292
371
|
}
|
|
372
|
+
log.trace('createUser', 'Final result:', {
|
|
373
|
+
id: result.id,
|
|
374
|
+
permissionsAssigned: result.permissionsAssigned
|
|
375
|
+
});
|
|
293
376
|
// Final serialization check before returning
|
|
294
377
|
return JSON.parse(JSON.stringify(result));
|
|
295
378
|
}
|
|
296
379
|
catch (error) {
|
|
297
|
-
|
|
380
|
+
log.error('createUser', 'Error:', error);
|
|
298
381
|
handleClerkError(error, 'Failed to create user');
|
|
299
382
|
}
|
|
300
383
|
});
|
|
301
384
|
export const updateUser = command('unchecked', async (options) => {
|
|
302
385
|
const { userId, userData } = options;
|
|
386
|
+
log.trace('updateUser', 'Called for userId:', userId, 'fields:', Object.keys(userData));
|
|
303
387
|
try {
|
|
304
388
|
const updateData = {};
|
|
305
389
|
if (userData.first_name !== undefined)
|
|
@@ -312,6 +396,7 @@ export const updateUser = command('unchecked', async (options) => {
|
|
|
312
396
|
if (userData.private_metadata !== undefined) {
|
|
313
397
|
updateData.private_metadata = userData.private_metadata;
|
|
314
398
|
}
|
|
399
|
+
log.trace('updateUser', 'Update payload:', updateData);
|
|
315
400
|
let result = await makeClerkRequest(`/users/${userId}`, {
|
|
316
401
|
method: 'PATCH',
|
|
317
402
|
body: JSON.stringify(updateData)
|
|
@@ -319,79 +404,95 @@ export const updateUser = command('unchecked', async (options) => {
|
|
|
319
404
|
// Ensure result is serializable
|
|
320
405
|
result = JSON.parse(JSON.stringify(result));
|
|
321
406
|
if (userData.permissions !== undefined) {
|
|
407
|
+
log.trace('updateUser', 'Updating permissions:', userData.permissions);
|
|
322
408
|
try {
|
|
323
409
|
await updateUserPermissions({ userId, permissions: userData.permissions });
|
|
324
410
|
}
|
|
325
411
|
catch (permError) {
|
|
326
|
-
|
|
412
|
+
log.error('updateUser', 'Failed to update permissions:', permError);
|
|
327
413
|
}
|
|
328
414
|
}
|
|
415
|
+
log.trace('updateUser', 'Done, result id:', result.id);
|
|
329
416
|
// Final serialization check before returning
|
|
330
417
|
return JSON.parse(JSON.stringify(result));
|
|
331
418
|
}
|
|
332
419
|
catch (error) {
|
|
333
|
-
|
|
420
|
+
log.error('updateUser', 'Error:', error);
|
|
334
421
|
handleClerkError(error, 'Failed to update user');
|
|
335
422
|
}
|
|
336
423
|
});
|
|
337
424
|
export const deleteUser = command('unchecked', async (userId) => {
|
|
425
|
+
log.trace('deleteUser', 'Called for userId:', userId);
|
|
338
426
|
try {
|
|
339
427
|
await makeClerkRequest(`/users/${userId}`, {
|
|
340
428
|
method: 'DELETE'
|
|
341
429
|
});
|
|
430
|
+
log.trace('deleteUser', 'Deleted successfully');
|
|
342
431
|
}
|
|
343
432
|
catch (error) {
|
|
344
|
-
|
|
433
|
+
log.error('deleteUser', 'Error:', error);
|
|
345
434
|
throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
346
435
|
}
|
|
347
436
|
});
|
|
348
437
|
export const deleteUsers = command('unchecked', async (userIds) => {
|
|
438
|
+
log.trace('deleteUsers', 'Called for userIds:', userIds);
|
|
349
439
|
try {
|
|
350
440
|
await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
|
|
351
441
|
method: 'DELETE'
|
|
352
442
|
})));
|
|
443
|
+
log.trace('deleteUsers', 'All', userIds.length, 'users deleted successfully');
|
|
353
444
|
}
|
|
354
445
|
catch (error) {
|
|
355
|
-
|
|
446
|
+
log.error('deleteUsers', 'Error:', error);
|
|
356
447
|
throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
357
448
|
}
|
|
358
449
|
});
|
|
359
450
|
async function fetchUserPermissions(email) {
|
|
451
|
+
log.trace('fetchUserPermissions', 'Called for email:', email);
|
|
360
452
|
try {
|
|
361
453
|
const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
|
|
454
|
+
log.trace('fetchUserPermissions', 'Raw keys data:', userData);
|
|
362
455
|
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
363
456
|
userData.data.data = userData.data.data.filter((key) => key.status === 'active');
|
|
457
|
+
log.trace('fetchUserPermissions', 'Active keys count:', userData.data.data.length);
|
|
364
458
|
}
|
|
365
459
|
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
366
460
|
// Deduplicate permissions by using a Set
|
|
367
461
|
const allPermissions = userData.data.data.flatMap((key) => key.scopes || []);
|
|
368
462
|
const dedupedPermissions = Array.from(new Set(allPermissions));
|
|
463
|
+
log.trace('fetchUserPermissions', 'Deduped permissions:', dedupedPermissions);
|
|
369
464
|
return dedupedPermissions;
|
|
370
465
|
}
|
|
371
466
|
else if (userData?.scopes) {
|
|
372
467
|
const permissions = Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
|
|
468
|
+
log.trace('fetchUserPermissions', 'Permissions from scopes field:', permissions);
|
|
373
469
|
return permissions;
|
|
374
470
|
}
|
|
471
|
+
log.trace('fetchUserPermissions', 'No permissions found, returning []');
|
|
375
472
|
return [];
|
|
376
473
|
}
|
|
377
474
|
catch (error) {
|
|
378
|
-
|
|
475
|
+
log.error('fetchUserPermissions', 'Error fetching user permissions:', error);
|
|
476
|
+
log.trace('fetchUserPermissions', 'Falling back to full keys search');
|
|
379
477
|
try {
|
|
380
478
|
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
381
479
|
const userKey = allKeysData.data.data.find((key) => key.sub === email && key.client_id === CLIENT_ID && key.status === 'active');
|
|
382
480
|
if (userKey) {
|
|
383
481
|
const permissions = Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
|
|
482
|
+
log.trace('fetchUserPermissions', 'Found via fallback:', permissions);
|
|
384
483
|
return permissions;
|
|
385
484
|
}
|
|
485
|
+
log.trace('fetchUserPermissions', 'No key found in fallback search');
|
|
386
486
|
return [];
|
|
387
487
|
}
|
|
388
488
|
catch (searchError) {
|
|
389
|
-
|
|
489
|
+
log.error('fetchUserPermissions', 'Error searching for user by sub:', searchError);
|
|
390
490
|
throw new Error('Failed to fetch user permissions');
|
|
391
491
|
}
|
|
392
492
|
}
|
|
393
493
|
}
|
|
394
494
|
export const getUserPermissions = query('unchecked', async (userId) => {
|
|
495
|
+
log.trace('getUserPermissions', 'Called for userId:', userId);
|
|
395
496
|
try {
|
|
396
497
|
// Fetch user from Clerk to get email
|
|
397
498
|
const user = await makeClerkRequest(`/users/${userId}`);
|
|
@@ -399,12 +500,14 @@ export const getUserPermissions = query('unchecked', async (userId) => {
|
|
|
399
500
|
if (!email) {
|
|
400
501
|
throw new Error('User has no email address');
|
|
401
502
|
}
|
|
503
|
+
log.trace('getUserPermissions', 'Resolved email:', email);
|
|
402
504
|
const permissions = await fetchUserPermissions(email);
|
|
505
|
+
log.trace('getUserPermissions', 'Returning permissions:', permissions);
|
|
403
506
|
// Ensure permissions array is serializable
|
|
404
507
|
return JSON.parse(JSON.stringify(permissions));
|
|
405
508
|
}
|
|
406
509
|
catch (error) {
|
|
407
|
-
|
|
510
|
+
log.error('getUserPermissions', 'Error:', error);
|
|
408
511
|
throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
409
512
|
}
|
|
410
513
|
});
|
|
@@ -419,6 +522,7 @@ async function refreshTokenIfSelfUpdate(userId) {
|
|
|
419
522
|
return;
|
|
420
523
|
const currentUser = getAuth();
|
|
421
524
|
const isSelfUpdate = currentUser?.userId === userId;
|
|
525
|
+
log.trace('refreshTokenIfSelfUpdate', 'isSelfUpdate:', isSelfUpdate);
|
|
422
526
|
if (isSelfUpdate) {
|
|
423
527
|
try {
|
|
424
528
|
// Try to dynamically import token manager - this is app-specific
|
|
@@ -431,6 +535,7 @@ async function refreshTokenIfSelfUpdate(userId) {
|
|
|
431
535
|
tokenManager.clearTokenCookies(event.cookies);
|
|
432
536
|
const newToken = await tokenManager.getAccessToken(event.cookies, event, true);
|
|
433
537
|
tokenManager.setClientAccessibleToken(event.cookies, newToken);
|
|
538
|
+
log.trace('refreshTokenIfSelfUpdate', 'Token refreshed successfully');
|
|
434
539
|
}
|
|
435
540
|
}
|
|
436
541
|
catch {
|
|
@@ -444,6 +549,7 @@ async function refreshTokenIfSelfUpdate(userId) {
|
|
|
444
549
|
}
|
|
445
550
|
export const updateUserPermissions = command('unchecked', async (options) => {
|
|
446
551
|
const { userId, permissions } = options;
|
|
552
|
+
log.trace('updateUserPermissions', 'Called for userId:', userId, 'permissions:', permissions);
|
|
447
553
|
try {
|
|
448
554
|
// Validate permissions - require at least one permission scope
|
|
449
555
|
if (permissions.length === 0) {
|
|
@@ -455,11 +561,14 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
455
561
|
if (!email) {
|
|
456
562
|
throw new Error('User has no email address');
|
|
457
563
|
}
|
|
564
|
+
log.trace('updateUserPermissions', 'Resolved email:', email);
|
|
458
565
|
// Fetch user's active keys
|
|
459
566
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
|
|
460
567
|
const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
|
|
568
|
+
log.trace('updateUserPermissions', 'Active keys count:', userKeys.length);
|
|
461
569
|
if (userKeys.length === 0) {
|
|
462
570
|
// No active key exists, create new one
|
|
571
|
+
log.trace('updateUserPermissions', 'No active key, creating new one');
|
|
463
572
|
const newKeyResult = await createUserPermissions(email, permissions);
|
|
464
573
|
const newApiKey = newKeyResult?.data?.key;
|
|
465
574
|
if (newApiKey) {
|
|
@@ -472,11 +581,13 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
472
581
|
}
|
|
473
582
|
})
|
|
474
583
|
});
|
|
584
|
+
log.trace('updateUserPermissions', 'New key stored in Clerk metadata');
|
|
475
585
|
}
|
|
476
586
|
}
|
|
477
587
|
else {
|
|
478
588
|
// Use PUT to update existing key (per Mako Auth API spec)
|
|
479
589
|
const keyId = userKeys[0].id;
|
|
590
|
+
log.trace('updateUserPermissions', 'Updating existing key:', keyId);
|
|
480
591
|
// Get the API key string before updating
|
|
481
592
|
const keyData = await makeAdminRequest(`/admin/keys/${keyId}`);
|
|
482
593
|
const apiKeyString = keyData?.data?.key;
|
|
@@ -486,45 +597,52 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
486
597
|
scopes: permissions
|
|
487
598
|
})
|
|
488
599
|
});
|
|
600
|
+
log.trace('updateUserPermissions', 'Key updated, verifying token...');
|
|
489
601
|
// Verify the token has updated scopes
|
|
490
602
|
if (apiKeyString) {
|
|
491
603
|
try {
|
|
492
604
|
const verification = await verifyApiKeyToken(apiKeyString);
|
|
493
|
-
|
|
605
|
+
log.info('updateUserPermissions', 'Key verification:', verification);
|
|
494
606
|
if (verification.valid) {
|
|
495
607
|
// Check if the scopes match what we expect
|
|
496
608
|
// Note: permissions.length > 0 is guaranteed by validation above
|
|
497
609
|
const scopesMatch = permissions.length > 0 &&
|
|
498
610
|
permissions.every((perm) => verification.scopes?.includes(perm));
|
|
499
611
|
if (!scopesMatch) {
|
|
500
|
-
|
|
612
|
+
log.warn('updateUserPermissions', 'Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
log.trace('updateUserPermissions', 'Scopes match confirmed');
|
|
501
616
|
}
|
|
502
617
|
}
|
|
503
618
|
else {
|
|
504
|
-
|
|
619
|
+
log.warn('updateUserPermissions', 'Token verification failed:', verification.error);
|
|
505
620
|
}
|
|
506
621
|
}
|
|
507
622
|
catch (verifyError) {
|
|
508
|
-
|
|
623
|
+
log.warn('updateUserPermissions', 'Could not verify token:', verifyError);
|
|
509
624
|
}
|
|
510
625
|
}
|
|
511
626
|
// Clean up any extra keys (there should only be one)
|
|
512
627
|
if (userKeys.length > 1) {
|
|
628
|
+
log.trace('updateUserPermissions', 'Cleaning up', userKeys.length - 1, 'extra keys');
|
|
513
629
|
await Promise.all(userKeys.slice(1).map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
|
|
514
630
|
method: 'DELETE'
|
|
515
631
|
}).catch((err) => {
|
|
516
|
-
|
|
632
|
+
log.warn('updateUserPermissions', `Failed to delete extra key ${key.id}:`, err);
|
|
517
633
|
})));
|
|
518
634
|
}
|
|
519
635
|
}
|
|
520
636
|
await refreshTokenIfSelfUpdate(userId);
|
|
637
|
+
log.trace('updateUserPermissions', 'Done');
|
|
521
638
|
}
|
|
522
639
|
catch (error) {
|
|
523
|
-
|
|
640
|
+
log.error('updateUserPermissions', 'Error:', error);
|
|
524
641
|
throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
525
642
|
}
|
|
526
643
|
});
|
|
527
644
|
export const generateApiKey = command('unchecked', async (options) => {
|
|
645
|
+
log.trace('generateApiKey', 'Called with userId:', options.userId, 'permissions:', options.permissions, 'revokeOld:', options.revokeOld);
|
|
528
646
|
try {
|
|
529
647
|
// No default permissions - require explicit permissions to be passed
|
|
530
648
|
if (options.permissions.length === 0) {
|
|
@@ -536,9 +654,11 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
536
654
|
if (!email) {
|
|
537
655
|
throw new Error('User has no email address');
|
|
538
656
|
}
|
|
657
|
+
log.trace('generateApiKey', 'Resolved email:', email);
|
|
539
658
|
// Check if user has existing active key
|
|
540
659
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
|
|
541
660
|
const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
|
|
661
|
+
log.trace('generateApiKey', 'Existing active keys:', userKeys.length);
|
|
542
662
|
let newApiKey;
|
|
543
663
|
let wasRotated = false;
|
|
544
664
|
let oldApiKey;
|
|
@@ -547,8 +667,10 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
547
667
|
if (userKeys.length > 0 && options.revokeOld) {
|
|
548
668
|
// Use rotate endpoint (per Mako Auth API spec)
|
|
549
669
|
const keyId = userKeys[0].id;
|
|
670
|
+
log.trace('generateApiKey', 'Rotating key:', keyId);
|
|
550
671
|
// Get the old API key from Clerk's private_metadata
|
|
551
672
|
oldApiKey = currentUser?.private_metadata?.mako_api_key;
|
|
673
|
+
log.trace('generateApiKey', 'Old key exists in metadata:', !!oldApiKey);
|
|
552
674
|
const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
|
|
553
675
|
method: 'POST',
|
|
554
676
|
body: JSON.stringify({
|
|
@@ -561,43 +683,48 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
561
683
|
if (!newApiKey) {
|
|
562
684
|
throw new Error('Failed to rotate API key - no key in response');
|
|
563
685
|
}
|
|
686
|
+
log.trace('generateApiKey', 'Rotation successful, new key obtained');
|
|
564
687
|
// Verify old key is revoked
|
|
565
688
|
if (oldApiKey) {
|
|
566
689
|
try {
|
|
567
690
|
const oldKeyVerification = await verifyApiKeyToken(oldApiKey);
|
|
568
|
-
|
|
691
|
+
log.info('generateApiKey', 'Old key verification:', oldKeyVerification.valid ? 'Still valid (unexpected)' : 'Revoked (expected)');
|
|
569
692
|
if (oldKeyVerification.valid) {
|
|
570
|
-
|
|
693
|
+
log.warn('generateApiKey', 'Old API key still valid after rotation');
|
|
571
694
|
}
|
|
572
695
|
}
|
|
573
696
|
catch (verifyError) {
|
|
574
|
-
|
|
697
|
+
log.warn('generateApiKey', 'Could not verify old key revocation:', verifyError);
|
|
575
698
|
}
|
|
576
699
|
}
|
|
577
700
|
// Verify new key works with correct scopes
|
|
578
701
|
try {
|
|
579
702
|
const newKeyVerification = await verifyApiKeyToken(newApiKey);
|
|
580
|
-
|
|
703
|
+
log.info('generateApiKey', 'New key verification:', newKeyVerification);
|
|
581
704
|
if (newKeyVerification.valid) {
|
|
582
705
|
// Check if the scopes match what we expect
|
|
583
706
|
const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
|
|
584
707
|
if (!scopesMatch) {
|
|
585
|
-
|
|
708
|
+
log.warn('generateApiKey', 'Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
|
|
586
709
|
verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
|
|
587
710
|
}
|
|
711
|
+
else {
|
|
712
|
+
log.trace('generateApiKey', 'New key scopes verified successfully');
|
|
713
|
+
}
|
|
588
714
|
}
|
|
589
715
|
else {
|
|
590
|
-
|
|
716
|
+
log.warn('generateApiKey', 'New key verification failed:', newKeyVerification.error);
|
|
591
717
|
verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
|
|
592
718
|
}
|
|
593
719
|
}
|
|
594
720
|
catch (verifyError) {
|
|
595
|
-
|
|
721
|
+
log.warn('generateApiKey', 'Could not verify new key:', verifyError);
|
|
596
722
|
verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
|
|
597
723
|
}
|
|
598
724
|
}
|
|
599
725
|
else {
|
|
600
726
|
// Create new key if none exists or revokeOld is false
|
|
727
|
+
log.trace('generateApiKey', 'Creating new key (no rotation)');
|
|
601
728
|
const createData = await createUserPermissions(email, options.permissions);
|
|
602
729
|
if (!createData) {
|
|
603
730
|
throw new Error('Failed to create admin key');
|
|
@@ -606,6 +733,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
606
733
|
if (!newApiKey) {
|
|
607
734
|
throw new Error('Failed to generate API key - no key in response');
|
|
608
735
|
}
|
|
736
|
+
log.trace('generateApiKey', 'New key created');
|
|
609
737
|
}
|
|
610
738
|
// Update Clerk profile with new key
|
|
611
739
|
try {
|
|
@@ -623,11 +751,12 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
623
751
|
}
|
|
624
752
|
})
|
|
625
753
|
});
|
|
754
|
+
log.trace('generateApiKey', 'Clerk metadata updated with new key');
|
|
626
755
|
}
|
|
627
756
|
}
|
|
628
757
|
catch (clerkError) {
|
|
629
|
-
|
|
630
|
-
|
|
758
|
+
log.error('generateApiKey', 'Failed to update Clerk profile:', clerkError);
|
|
759
|
+
log.warn('generateApiKey', 'Key generated but could not update Clerk profile');
|
|
631
760
|
}
|
|
632
761
|
const result = {
|
|
633
762
|
success: true,
|
|
@@ -635,18 +764,20 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
635
764
|
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
|
|
636
765
|
verificationWarning
|
|
637
766
|
};
|
|
767
|
+
log.trace('generateApiKey', 'Done, wasRotated:', wasRotated);
|
|
638
768
|
return JSON.parse(JSON.stringify(result));
|
|
639
769
|
}
|
|
640
770
|
catch (error) {
|
|
641
|
-
|
|
771
|
+
log.error('generateApiKey', 'Error:', error);
|
|
642
772
|
throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
643
773
|
}
|
|
644
774
|
});
|
|
645
775
|
export const verifyToken = command('unchecked', async (options) => {
|
|
776
|
+
log.trace('verifyToken', 'Called');
|
|
646
777
|
try {
|
|
647
778
|
const result = await verifyApiKeyToken(options.apiKey);
|
|
648
|
-
|
|
649
|
-
|
|
779
|
+
log.info('verifyToken', 'Current scopes:', result.scopes);
|
|
780
|
+
log.info('verifyToken', 'Sub:', result.sub);
|
|
650
781
|
// Also return the issued token for debugging
|
|
651
782
|
if (result.valid) {
|
|
652
783
|
try {
|
|
@@ -660,26 +791,30 @@ export const verifyToken = command('unchecked', async (options) => {
|
|
|
660
791
|
valid: result.valid,
|
|
661
792
|
scopes: result.scopes,
|
|
662
793
|
sub: result.sub,
|
|
794
|
+
client_id: result.client_id,
|
|
663
795
|
token: tokenResult.data?.data?.access_token
|
|
664
796
|
};
|
|
797
|
+
log.trace('verifyToken', 'Returning valid result with token');
|
|
665
798
|
// Ensure result is serializable
|
|
666
799
|
return JSON.parse(JSON.stringify(finalResult));
|
|
667
800
|
}
|
|
668
801
|
catch (tokenError) {
|
|
669
|
-
|
|
802
|
+
log.warn('verifyToken', 'Could not fetch token:', tokenError);
|
|
670
803
|
// Return result without token
|
|
671
804
|
return JSON.parse(JSON.stringify({
|
|
672
805
|
valid: result.valid,
|
|
673
806
|
scopes: result.scopes,
|
|
674
|
-
sub: result.sub
|
|
807
|
+
sub: result.sub,
|
|
808
|
+
client_id: result.client_id
|
|
675
809
|
}));
|
|
676
810
|
}
|
|
677
811
|
}
|
|
812
|
+
log.trace('verifyToken', 'Returning invalid result');
|
|
678
813
|
// Ensure result is serializable
|
|
679
814
|
return JSON.parse(JSON.stringify(result));
|
|
680
815
|
}
|
|
681
816
|
catch (error) {
|
|
682
|
-
|
|
817
|
+
log.error('verifyToken', 'Error:', error);
|
|
683
818
|
const errorResult = {
|
|
684
819
|
valid: false,
|
|
685
820
|
error: error instanceof Error ? error.message : 'Unknown error during verification'
|
package/dist/index.d.ts
CHANGED
|
@@ -989,6 +989,7 @@ export interface UserModalProps {
|
|
|
989
989
|
user?: User | null;
|
|
990
990
|
roles?: Role[];
|
|
991
991
|
adapter?: UserManagementAdapter;
|
|
992
|
+
loadingPermissions?: boolean;
|
|
992
993
|
onSave: (user: User, mode: 'create' | 'edit') => void | Promise<void>;
|
|
993
994
|
onClose?: () => void;
|
|
994
995
|
class?: ClassValue;
|
|
@@ -327,35 +327,41 @@
|
|
|
327
327
|
|
|
328
328
|
<tbody class={tbodyClasses}>
|
|
329
329
|
{#if loading}
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
330
|
+
{#each Array.from({ length: Math.min(internalPageSize, 5) }, (__, i) => i) as rowIdx (rowIdx)}
|
|
331
|
+
<tr class={cn(trClass, rowIdx % 2 === 1 && striped ? 'bg-default-50' : '')}>
|
|
332
|
+
{#if selectable}
|
|
333
|
+
<td class={cn(tdClasses, 'text-center')}>
|
|
334
|
+
<div class="bg-default-200 mx-auto h-4 w-4 animate-pulse rounded"></div>
|
|
335
|
+
</td>
|
|
336
|
+
{/if}
|
|
337
|
+
{#each columns as column, colIdx (column.key)}
|
|
338
|
+
<td class={cn(tdClasses, column.class)}>
|
|
339
|
+
{#if colIdx === 0}
|
|
340
|
+
<div class="flex items-center gap-3">
|
|
341
|
+
<div
|
|
342
|
+
class="bg-default-200 h-8 w-8 shrink-0 animate-pulse rounded-full"
|
|
343
|
+
></div>
|
|
344
|
+
<div class="flex-1 space-y-2">
|
|
345
|
+
<div class="bg-default-200 h-3.5 w-28 animate-pulse rounded"></div>
|
|
346
|
+
<div class="bg-default-100 h-3 w-20 animate-pulse rounded"></div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
{:else if colIdx === columns.length - 1}
|
|
350
|
+
<div class="flex items-center justify-end gap-2">
|
|
351
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
352
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
353
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
354
|
+
</div>
|
|
355
|
+
{:else}
|
|
356
|
+
<div
|
|
357
|
+
class="bg-default-200 h-3.5 animate-pulse rounded"
|
|
358
|
+
style="width: {60 + ((rowIdx * 17 + colIdx * 31) % 40)}%"
|
|
359
|
+
></div>
|
|
360
|
+
{/if}
|
|
361
|
+
</td>
|
|
362
|
+
{/each}
|
|
363
|
+
</tr>
|
|
364
|
+
{/each}
|
|
359
365
|
{:else if getPaginatedData().length === 0}
|
|
360
366
|
<tr>
|
|
361
367
|
<td
|
|
@@ -555,35 +561,41 @@
|
|
|
555
561
|
|
|
556
562
|
<tbody class={tbodyClasses}>
|
|
557
563
|
{#if loading}
|
|
558
|
-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
564
|
+
{#each Array.from({ length: Math.min(internalPageSize, 5) }, (__, i) => i) as rowIdx (rowIdx)}
|
|
565
|
+
<tr class={cn(trClass, rowIdx % 2 === 1 && striped ? 'bg-default-50' : '')}>
|
|
566
|
+
{#if selectable}
|
|
567
|
+
<td class={cn(tdClasses, 'text-center')}>
|
|
568
|
+
<div class="bg-default-200 mx-auto h-4 w-4 animate-pulse rounded"></div>
|
|
569
|
+
</td>
|
|
570
|
+
{/if}
|
|
571
|
+
{#each columns as column, colIdx (column.key)}
|
|
572
|
+
<td class={cn(tdClasses, column.class)}>
|
|
573
|
+
{#if colIdx === 0}
|
|
574
|
+
<div class="flex items-center gap-3">
|
|
575
|
+
<div
|
|
576
|
+
class="bg-default-200 h-8 w-8 shrink-0 animate-pulse rounded-full"
|
|
577
|
+
></div>
|
|
578
|
+
<div class="flex-1 space-y-2">
|
|
579
|
+
<div class="bg-default-200 h-3.5 w-28 animate-pulse rounded"></div>
|
|
580
|
+
<div class="bg-default-100 h-3 w-20 animate-pulse rounded"></div>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
{:else if colIdx === columns.length - 1}
|
|
584
|
+
<div class="flex items-center justify-end gap-2">
|
|
585
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
586
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
587
|
+
<div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
|
|
588
|
+
</div>
|
|
589
|
+
{:else}
|
|
590
|
+
<div
|
|
591
|
+
class="bg-default-200 h-3.5 animate-pulse rounded"
|
|
592
|
+
style="width: {60 + ((rowIdx * 17 + colIdx * 31) % 40)}%"
|
|
593
|
+
></div>
|
|
594
|
+
{/if}
|
|
595
|
+
</td>
|
|
596
|
+
{/each}
|
|
597
|
+
</tr>
|
|
598
|
+
{/each}
|
|
587
599
|
{:else if getPaginatedData().length === 0}
|
|
588
600
|
<tr>
|
|
589
601
|
<td
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
+
import { toast } from 'svelte-sonner';
|
|
3
4
|
import { PageHeader, Button, cn } from '../index.js';
|
|
4
5
|
import UserTable from './UserTable.svelte';
|
|
5
6
|
import UserModal from './UserModal.svelte';
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
// Internal state
|
|
18
19
|
let users = $state<User[]>([]);
|
|
19
20
|
let totalUsers = $state(0);
|
|
20
|
-
let loading = $state(
|
|
21
|
+
let loading = $state(true);
|
|
21
22
|
let currentPage = $state(1);
|
|
22
23
|
let pageSize = $state(10);
|
|
23
24
|
let sortBy = $state<string | null>(null);
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
let showEditCreateModal = $state(false);
|
|
30
31
|
let showViewModal = $state(false);
|
|
31
32
|
let selectedUser = $state<User | null>(null);
|
|
33
|
+
let loadingPermissions = $state(false);
|
|
32
34
|
|
|
33
35
|
// Bulk action states
|
|
34
36
|
let selectedUsers = new SvelteSet<string>();
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
totalUsers = result.totalUsers;
|
|
53
55
|
} catch (error) {
|
|
54
56
|
console.error('Error loading users:', error);
|
|
57
|
+
toast.error('Failed to load users');
|
|
55
58
|
} finally {
|
|
56
59
|
loading = false;
|
|
57
60
|
}
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
showViewModal = true;
|
|
89
92
|
// Fetch fresh permissions for the user
|
|
90
93
|
if (user.id) {
|
|
94
|
+
loadingPermissions = true;
|
|
91
95
|
try {
|
|
92
96
|
const permissions = await adapter.getUserPermissions(user.id);
|
|
93
97
|
selectedUser = {
|
|
@@ -96,6 +100,9 @@
|
|
|
96
100
|
};
|
|
97
101
|
} catch (error) {
|
|
98
102
|
console.error('Error fetching user permissions:', error);
|
|
103
|
+
toast.error('Failed to load user permissions');
|
|
104
|
+
} finally {
|
|
105
|
+
loadingPermissions = false;
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
}
|
|
@@ -105,6 +112,7 @@
|
|
|
105
112
|
showEditCreateModal = true;
|
|
106
113
|
// Fetch fresh permissions for the user
|
|
107
114
|
if (user.id) {
|
|
115
|
+
loadingPermissions = true;
|
|
108
116
|
try {
|
|
109
117
|
const permissions = await adapter.getUserPermissions(user.id);
|
|
110
118
|
selectedUser = {
|
|
@@ -113,6 +121,9 @@
|
|
|
113
121
|
};
|
|
114
122
|
} catch (error) {
|
|
115
123
|
console.error('Error fetching user permissions:', error);
|
|
124
|
+
toast.error('Failed to load user permissions');
|
|
125
|
+
} finally {
|
|
126
|
+
loadingPermissions = false;
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
}
|
|
@@ -129,6 +140,17 @@
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
// Sync any changes made to selectedUser (e.g. API key regeneration) back into the users array
|
|
144
|
+
function handleModalClose() {
|
|
145
|
+
if (selectedUser) {
|
|
146
|
+
const idx = users.findIndex((u) => u.id === selectedUser!.id);
|
|
147
|
+
if (idx !== -1) {
|
|
148
|
+
users[idx] = { ...selectedUser };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
selectedUser = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
132
154
|
// Save handlers
|
|
133
155
|
async function handleUserSave(user: User, mode: 'create' | 'edit') {
|
|
134
156
|
try {
|
|
@@ -143,6 +165,7 @@
|
|
|
143
165
|
await refreshUsersQuery();
|
|
144
166
|
} catch (error) {
|
|
145
167
|
console.error('Error saving user:', error);
|
|
168
|
+
toast.error(mode === 'create' ? 'Failed to create user' : 'Failed to update user');
|
|
146
169
|
// Reload on error to restore correct state
|
|
147
170
|
await loadUsers();
|
|
148
171
|
throw error;
|
|
@@ -175,6 +198,7 @@
|
|
|
175
198
|
await refreshUsersQuery();
|
|
176
199
|
} catch (error) {
|
|
177
200
|
console.error('Error deleting user:', error);
|
|
201
|
+
toast.error('Failed to delete user');
|
|
178
202
|
// Reload on error to restore correct state
|
|
179
203
|
await loadUsers();
|
|
180
204
|
throw error;
|
|
@@ -217,6 +241,7 @@
|
|
|
217
241
|
await refreshUsersQuery();
|
|
218
242
|
} catch (error) {
|
|
219
243
|
console.error('Error deleting users:', error);
|
|
244
|
+
toast.error('Failed to delete users');
|
|
220
245
|
// Reload on error to restore correct state
|
|
221
246
|
await loadUsers();
|
|
222
247
|
throw error;
|
|
@@ -296,7 +321,7 @@
|
|
|
296
321
|
bind:user={selectedUser}
|
|
297
322
|
{permissions}
|
|
298
323
|
onEdit={handleEditFromView}
|
|
299
|
-
onClose={
|
|
324
|
+
onClose={handleModalClose}
|
|
300
325
|
/>
|
|
301
326
|
|
|
302
327
|
<!-- User Edit/Create Modal -->
|
|
@@ -305,7 +330,8 @@
|
|
|
305
330
|
bind:user={selectedUser}
|
|
306
331
|
{roles}
|
|
307
332
|
{adapter}
|
|
333
|
+
{loadingPermissions}
|
|
308
334
|
onSave={handleUserSave}
|
|
309
|
-
onClose={
|
|
335
|
+
onClose={handleModalClose}
|
|
310
336
|
/>
|
|
311
337
|
</div>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
user = $bindable(),
|
|
18
18
|
roles = [],
|
|
19
19
|
adapter,
|
|
20
|
+
loadingPermissions = false,
|
|
20
21
|
onSave,
|
|
21
22
|
onClose,
|
|
22
23
|
class: className
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
valid?: boolean;
|
|
37
38
|
scopes?: string[];
|
|
38
39
|
sub?: string;
|
|
40
|
+
client_id?: string;
|
|
39
41
|
error?: string;
|
|
40
42
|
} | null>(null);
|
|
41
43
|
let initialRole = $state<string>('');
|
|
@@ -490,6 +492,11 @@
|
|
|
490
492
|
Sub: {tokenVerification.sub}
|
|
491
493
|
</p>
|
|
492
494
|
{/if}
|
|
495
|
+
{#if tokenVerification.client_id}
|
|
496
|
+
<p class="text-success-700 mt-1 text-xs">
|
|
497
|
+
Client ID: {tokenVerification.client_id}
|
|
498
|
+
</p>
|
|
499
|
+
{/if}
|
|
493
500
|
{#if tokenVerification.scopes && tokenVerification.scopes.length > 0}
|
|
494
501
|
<p class="text-success-700 mt-1 text-xs">
|
|
495
502
|
Scopes: {tokenVerification.scopes.join(', ')}
|
|
@@ -583,7 +590,24 @@
|
|
|
583
590
|
{/if}
|
|
584
591
|
|
|
585
592
|
<!-- Permission Preview -->
|
|
586
|
-
{#if
|
|
593
|
+
{#if loadingPermissions && mode === 'edit'}
|
|
594
|
+
<div>
|
|
595
|
+
<div class="bg-default-200 mb-2 h-4 w-32 animate-pulse rounded"></div>
|
|
596
|
+
<div class="bg-default-50 rounded-lg p-3">
|
|
597
|
+
<div class="space-y-3">
|
|
598
|
+
{#each [0, 1, 2] as i (i)}
|
|
599
|
+
<div class="flex items-center gap-2">
|
|
600
|
+
<div class="bg-default-200 h-1.5 w-1.5 shrink-0 animate-pulse rounded-full"></div>
|
|
601
|
+
<div
|
|
602
|
+
class="bg-default-200 h-3.5 animate-pulse rounded"
|
|
603
|
+
style="width: {50 + i * 20}%"
|
|
604
|
+
></div>
|
|
605
|
+
</div>
|
|
606
|
+
{/each}
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
{:else if formData.permissions && formData.permissions.length > 0}
|
|
587
611
|
<div>
|
|
588
612
|
<h4 class="text-default-700 mb-2 text-sm font-medium">
|
|
589
613
|
Permissions ({formData.permissions.length})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.11",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -23,14 +23,6 @@
|
|
|
23
23
|
"test:e2e:debug": "playwright test --debug",
|
|
24
24
|
"test:e2e:report": "playwright show-report",
|
|
25
25
|
"test": "npm run test:unit -- --run",
|
|
26
|
-
"pub:minor": "git add . && git commit -m \"chore: prepare for publish minor\" && npm version minor && git push --follow-tags && npm publish",
|
|
27
|
-
"pub:patch": "git add . && git commit -m \"chore: prepare for publish patch\" && npm version patch && git push --follow-tags && npm publish",
|
|
28
|
-
"pub:prelish": "git add . && git commit -m \"chore: prepare for publish premajor\" && npm version premajor && git push --follow-tags && npm publish",
|
|
29
|
-
"pub:major": "git add . && git commit -m \"chore: prepare for publish major\" && npm version major && git push --follow-tags && npm publish",
|
|
30
|
-
"pub:preminor": "git add . && git commit -m \"chore: prepare for publish preminor\" && npm version preminor && git push --follow-tags && npm publish",
|
|
31
|
-
"pub:prepatch": "git add . && git commit -m \"chore: prepare for publish prepatch\" && npm version prepatch && git push --follow-tags && npm publish",
|
|
32
|
-
"pub:prerelease": "git add . && git commit -m \"chore: prepare for publish prerelease\" && npm version prerelease && git push --follow-tags && npm publish",
|
|
33
|
-
"pub:from-git": "git add . && git commit -m \"chore: prepare for publish from-git\" && npm version from-git && git push --follow-tags && npm publish",
|
|
34
26
|
"storybook": "storybook dev -p 6006",
|
|
35
27
|
"build-storybook": "storybook build"
|
|
36
28
|
},
|