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