@makolabs/ripple 1.6.9 → 1.7.2
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/mock-user-management.js +8 -0
- package/dist/funcs/user-management.remote.js +24 -23
- package/dist/index.d.ts +5 -0
- package/dist/layout/sidebar/Sidebar.svelte +8 -1
- package/dist/user-management/UserManagement.svelte +5 -20
- package/dist/user-management/UserModal.svelte +26 -2
- package/package.json +5 -1
|
@@ -176,6 +176,10 @@ export async function getUserPermissions(userId) {
|
|
|
176
176
|
export async function updateUserPermissions(options) {
|
|
177
177
|
await delay();
|
|
178
178
|
const { userId, permissions } = options;
|
|
179
|
+
// Validate permissions - require at least one permission scope
|
|
180
|
+
if (permissions.length === 0) {
|
|
181
|
+
throw new Error('At least one permission scope is required');
|
|
182
|
+
}
|
|
179
183
|
const user = mockUsers.find((u) => u.id === userId);
|
|
180
184
|
if (!user) {
|
|
181
185
|
throw new Error(`User with ID ${userId} not found`);
|
|
@@ -188,6 +192,10 @@ export async function updateUserPermissions(options) {
|
|
|
188
192
|
*/
|
|
189
193
|
export async function generateApiKey(options) {
|
|
190
194
|
await delay();
|
|
195
|
+
// Validate permissions - require at least one permission scope
|
|
196
|
+
if (options.permissions.length === 0) {
|
|
197
|
+
throw new Error('At least one permission scope is required');
|
|
198
|
+
}
|
|
191
199
|
const { userId } = options;
|
|
192
200
|
const user = mockUsers.find((u) => u.id === userId);
|
|
193
201
|
if (!user) {
|
|
@@ -2,7 +2,6 @@ import { query, command } from '$app/server';
|
|
|
2
2
|
import { getRequestEvent } from '$app/server';
|
|
3
3
|
import { env } from '$env/dynamic/private';
|
|
4
4
|
const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
|
|
5
|
-
const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
|
|
6
5
|
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
7
6
|
function handleClerkError(error, defaultMessage) {
|
|
8
7
|
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
@@ -147,10 +146,7 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
|
|
150
|
-
|
|
151
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
152
|
-
: permissions;
|
|
153
|
-
if (filteredPermissions.length === 0) {
|
|
149
|
+
if (permissions.length === 0) {
|
|
154
150
|
return null;
|
|
155
151
|
}
|
|
156
152
|
return await makeAdminRequest('/admin/keys', {
|
|
@@ -158,7 +154,7 @@ async function createUserPermissions(userId, permissions, clientId = CLIENT_ID)
|
|
|
158
154
|
body: JSON.stringify({
|
|
159
155
|
client_id: clientId,
|
|
160
156
|
sub: userId,
|
|
161
|
-
scopes:
|
|
157
|
+
scopes: permissions
|
|
162
158
|
})
|
|
163
159
|
});
|
|
164
160
|
}
|
|
@@ -420,12 +416,13 @@ async function refreshTokenIfSelfUpdate(userId) {
|
|
|
420
416
|
export const updateUserPermissions = command('unchecked', async (options) => {
|
|
421
417
|
const { userId, permissions } = options;
|
|
422
418
|
try {
|
|
419
|
+
// Validate permissions - require at least one permission scope
|
|
420
|
+
if (permissions.length === 0) {
|
|
421
|
+
throw new Error('At least one permission scope is required');
|
|
422
|
+
}
|
|
423
423
|
// Fetch user's active keys
|
|
424
424
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
425
425
|
const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
|
|
426
|
-
const filteredPermissions = PERMISSION_PREFIX
|
|
427
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
428
|
-
: permissions;
|
|
429
426
|
if (userKeys.length === 0) {
|
|
430
427
|
// No active key exists, create new one
|
|
431
428
|
const newKeyResult = await createUserPermissions(userId, permissions);
|
|
@@ -452,7 +449,7 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
452
449
|
await makeAdminRequest(`/admin/keys/${keyId}`, {
|
|
453
450
|
method: 'PUT',
|
|
454
451
|
body: JSON.stringify({
|
|
455
|
-
scopes:
|
|
452
|
+
scopes: permissions
|
|
456
453
|
})
|
|
457
454
|
});
|
|
458
455
|
// Verify the token has updated scopes
|
|
@@ -462,9 +459,11 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
462
459
|
console.log('[updateUserPermissions] Key verification:', verification);
|
|
463
460
|
if (verification.valid) {
|
|
464
461
|
// Check if the scopes match what we expect
|
|
465
|
-
|
|
462
|
+
// Note: permissions.length > 0 is guaranteed by validation above
|
|
463
|
+
const scopesMatch = permissions.length > 0 &&
|
|
464
|
+
permissions.every((perm) => verification.scopes?.includes(perm));
|
|
466
465
|
if (!scopesMatch) {
|
|
467
|
-
console.warn('[updateUserPermissions] Scopes mismatch. Expected:',
|
|
466
|
+
console.warn('[updateUserPermissions] Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
|
|
468
467
|
}
|
|
469
468
|
}
|
|
470
469
|
else {
|
|
@@ -493,12 +492,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
493
492
|
});
|
|
494
493
|
export const generateApiKey = command('unchecked', async (options) => {
|
|
495
494
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// Default to readonly permission if none provided (first-time key generation)
|
|
500
|
-
if (filteredPermissions.length === 0 && PERMISSION_PREFIX) {
|
|
501
|
-
filteredPermissions = [`${PERMISSION_PREFIX}readonly`];
|
|
495
|
+
// No default permissions - require explicit permissions to be passed
|
|
496
|
+
if (options.permissions.length === 0) {
|
|
497
|
+
throw new Error('At least one permission scope is required');
|
|
502
498
|
}
|
|
503
499
|
// Check if user has existing active key
|
|
504
500
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${options.userId}`);
|
|
@@ -507,6 +503,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
507
503
|
let wasRotated = false;
|
|
508
504
|
let oldApiKey;
|
|
509
505
|
let currentUser = null;
|
|
506
|
+
let verificationWarning;
|
|
510
507
|
if (userKeys.length > 0 && options.revokeOld) {
|
|
511
508
|
// Use rotate endpoint (per Mako Auth API spec)
|
|
512
509
|
const keyId = userKeys[0].id;
|
|
@@ -516,7 +513,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
516
513
|
const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
|
|
517
514
|
method: 'POST',
|
|
518
515
|
body: JSON.stringify({
|
|
519
|
-
scopes:
|
|
516
|
+
scopes: options.permissions
|
|
520
517
|
})
|
|
521
518
|
});
|
|
522
519
|
// Rotate endpoint returns key in data.key field
|
|
@@ -544,22 +541,25 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
544
541
|
console.log('[generateApiKey] New key verification:', newKeyVerification);
|
|
545
542
|
if (newKeyVerification.valid) {
|
|
546
543
|
// Check if the scopes match what we expect
|
|
547
|
-
const scopesMatch =
|
|
544
|
+
const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
|
|
548
545
|
if (!scopesMatch) {
|
|
549
|
-
console.warn('[generateApiKey] Scopes mismatch. Expected:',
|
|
546
|
+
console.warn('[generateApiKey] Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
|
|
547
|
+
verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
|
|
550
548
|
}
|
|
551
549
|
}
|
|
552
550
|
else {
|
|
553
551
|
console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
|
|
552
|
+
verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
|
|
554
553
|
}
|
|
555
554
|
}
|
|
556
555
|
catch (verifyError) {
|
|
557
556
|
console.warn('[generateApiKey] Could not verify new key:', verifyError);
|
|
557
|
+
verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
else {
|
|
561
561
|
// Create new key if none exists or revokeOld is false
|
|
562
|
-
const createData = await createUserPermissions(options.userId,
|
|
562
|
+
const createData = await createUserPermissions(options.userId, options.permissions);
|
|
563
563
|
if (!createData) {
|
|
564
564
|
throw new Error('Failed to create admin key');
|
|
565
565
|
}
|
|
@@ -593,7 +593,8 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
593
593
|
const result = {
|
|
594
594
|
success: true,
|
|
595
595
|
apiKey: newApiKey,
|
|
596
|
-
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully'
|
|
596
|
+
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
|
|
597
|
+
verificationWarning
|
|
597
598
|
};
|
|
598
599
|
return JSON.parse(JSON.stringify(result));
|
|
599
600
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -339,6 +339,10 @@ export interface NavItemProps {
|
|
|
339
339
|
export interface SidebarProps {
|
|
340
340
|
items?: NavigationItem[];
|
|
341
341
|
logo: LogoType;
|
|
342
|
+
/** Optional footer snippet rendered at the bottom of the sidebar */
|
|
343
|
+
footer?: Snippet<[{
|
|
344
|
+
collapsed: boolean;
|
|
345
|
+
}]>;
|
|
342
346
|
}
|
|
343
347
|
export { tv, cn } from './helper/cls.js';
|
|
344
348
|
export { isRouteActive } from './helper/nav.svelte.js';
|
|
@@ -1030,6 +1034,7 @@ export interface UserManagementAdapter {
|
|
|
1030
1034
|
success: boolean;
|
|
1031
1035
|
apiKey: string;
|
|
1032
1036
|
message: string;
|
|
1037
|
+
verificationWarning?: string;
|
|
1033
1038
|
}>;
|
|
1034
1039
|
verifyToken?: (options: {
|
|
1035
1040
|
apiKey: string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { isRouteActive } from '../../helper/nav.svelte.js';
|
|
6
6
|
import { resolve } from '$app/paths';
|
|
7
7
|
|
|
8
|
-
let { items = [], logo }: SidebarProps = $props();
|
|
8
|
+
let { items = [], logo, footer }: SidebarProps = $props();
|
|
9
9
|
let menubar: MenuBar = $state({
|
|
10
10
|
collapsed: false
|
|
11
11
|
});
|
|
@@ -178,6 +178,13 @@
|
|
|
178
178
|
{/each}
|
|
179
179
|
</nav>
|
|
180
180
|
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Footer slot for custom content -->
|
|
183
|
+
{#if footer}
|
|
184
|
+
<div class="shrink-0 border-t border-white/10 px-3 py-3">
|
|
185
|
+
{@render footer({ collapsed: menubar.collapsed })}
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
181
188
|
</div>
|
|
182
189
|
|
|
183
190
|
{#snippet ToggleIcon(classes = 'size-6 shrink-0 text-default-200')}
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
import UserTable from './UserTable.svelte';
|
|
5
5
|
import UserModal from './UserModal.svelte';
|
|
6
6
|
import UserViewModal from './UserViewModal.svelte';
|
|
7
|
-
import type { User, UserManagementProps, Role, Permission
|
|
7
|
+
import type { User, UserManagementProps, Role, Permission } from '../index.js';
|
|
8
8
|
import { SvelteSet } from 'svelte/reactivity';
|
|
9
|
-
import type { RemoteQuery } from '@sveltejs/kit';
|
|
10
9
|
|
|
11
10
|
let {
|
|
12
11
|
adapter,
|
|
@@ -60,13 +59,7 @@
|
|
|
60
59
|
|
|
61
60
|
// Refresh the query cache to get fresh data
|
|
62
61
|
async function refreshUsersQuery() {
|
|
63
|
-
|
|
64
|
-
page: currentPage,
|
|
65
|
-
pageSize,
|
|
66
|
-
sortBy: sortBy || undefined,
|
|
67
|
-
sortOrder: sortOrder || 'desc'
|
|
68
|
-
}) as RemoteQuery<GetUsersResult>;
|
|
69
|
-
await query.refresh();
|
|
62
|
+
await loadUsers();
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
// Handlers
|
|
@@ -146,10 +139,8 @@
|
|
|
146
139
|
} else {
|
|
147
140
|
await adapter.updateUser({ userId: user.id, userData: user });
|
|
148
141
|
}
|
|
149
|
-
//
|
|
142
|
+
// Refresh the users list
|
|
150
143
|
await refreshUsersQuery();
|
|
151
|
-
// Force refresh by reassigning state to trigger reactivity
|
|
152
|
-
await loadUsers();
|
|
153
144
|
} catch (error) {
|
|
154
145
|
console.error('Error saving user:', error);
|
|
155
146
|
// Reload on error to restore correct state
|
|
@@ -180,11 +171,8 @@
|
|
|
180
171
|
// Perform the actual delete operation
|
|
181
172
|
await adapter.deleteUser(userId);
|
|
182
173
|
|
|
183
|
-
// Invalidate the query cache by calling refresh() before loading
|
|
184
|
-
await refreshUsersQuery();
|
|
185
|
-
|
|
186
174
|
// Refresh to ensure we have the latest data
|
|
187
|
-
await
|
|
175
|
+
await refreshUsersQuery();
|
|
188
176
|
} catch (error) {
|
|
189
177
|
console.error('Error deleting user:', error);
|
|
190
178
|
// Reload on error to restore correct state
|
|
@@ -225,11 +213,8 @@
|
|
|
225
213
|
// Perform the actual delete operation
|
|
226
214
|
await adapter.deleteUsers(userIds);
|
|
227
215
|
|
|
228
|
-
// Invalidate the query cache by calling refresh() before loading
|
|
229
|
-
await refreshUsersQuery();
|
|
230
|
-
|
|
231
216
|
// Refresh to ensure we have the latest data
|
|
232
|
-
await
|
|
217
|
+
await refreshUsersQuery();
|
|
233
218
|
} catch (error) {
|
|
234
219
|
console.error('Error deleting users:', error);
|
|
235
220
|
// Reload on error to restore correct state
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
type Role,
|
|
10
10
|
getUserDisplayName
|
|
11
11
|
} from '../index.js';
|
|
12
|
+
import { toast } from 'svelte-sonner';
|
|
12
13
|
|
|
13
14
|
// Icons as simple SVGs
|
|
14
15
|
let {
|
|
@@ -222,6 +223,14 @@
|
|
|
222
223
|
}
|
|
223
224
|
};
|
|
224
225
|
}
|
|
226
|
+
|
|
227
|
+
// Show warning toast if new key verification failed
|
|
228
|
+
if (result.verificationWarning) {
|
|
229
|
+
toast.warning('API Key Verification Warning', {
|
|
230
|
+
description: result.verificationWarning,
|
|
231
|
+
duration: 8000 // 8 seconds for important warnings
|
|
232
|
+
});
|
|
233
|
+
}
|
|
225
234
|
} catch (error) {
|
|
226
235
|
console.error('Error regenerating API key:', error);
|
|
227
236
|
formErrors.apiKey = error instanceof Error ? error.message : 'Failed to regenerate API key';
|
|
@@ -276,7 +285,7 @@
|
|
|
276
285
|
contentclass="max-w-4xl"
|
|
277
286
|
class={cn(className)}
|
|
278
287
|
>
|
|
279
|
-
<form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6">
|
|
288
|
+
<form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6" data-testid="user-form">
|
|
280
289
|
<!-- Left Column: Profile Information -->
|
|
281
290
|
<div class="min-w-0 flex-1 space-y-4">
|
|
282
291
|
<div class="border-default-200 border-b pb-3">
|
|
@@ -296,6 +305,7 @@
|
|
|
296
305
|
? 'border-danger-300'
|
|
297
306
|
: 'border-default-300'}"
|
|
298
307
|
placeholder="First name"
|
|
308
|
+
data-testid="first-name-input"
|
|
299
309
|
required
|
|
300
310
|
/>
|
|
301
311
|
{#if formErrors.first_name}
|
|
@@ -316,6 +326,7 @@
|
|
|
316
326
|
? 'border-danger-300'
|
|
317
327
|
: 'border-default-300'}"
|
|
318
328
|
placeholder="Last name"
|
|
329
|
+
data-testid="last-name-input"
|
|
319
330
|
required
|
|
320
331
|
/>
|
|
321
332
|
{#if formErrors.last_name}
|
|
@@ -336,6 +347,7 @@
|
|
|
336
347
|
? 'border-danger-300'
|
|
337
348
|
: 'border-default-300'}"
|
|
338
349
|
placeholder="user@example.com"
|
|
350
|
+
data-testid="email-input"
|
|
339
351
|
required
|
|
340
352
|
/>
|
|
341
353
|
{#if formErrors.email}
|
|
@@ -358,6 +370,7 @@
|
|
|
358
370
|
disabled={verifyingToken}
|
|
359
371
|
class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-green-600 transition-colors hover:text-green-700 hover:underline disabled:cursor-not-allowed"
|
|
360
372
|
aria-label="Verify token"
|
|
373
|
+
data-testid="verify-token-button"
|
|
361
374
|
>
|
|
362
375
|
<svg
|
|
363
376
|
class="h-4 w-4 {verifyingToken ? 'animate-spin' : ''}"
|
|
@@ -384,6 +397,7 @@
|
|
|
384
397
|
formData.permissions.length === 0}
|
|
385
398
|
class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-700 hover:underline disabled:cursor-not-allowed"
|
|
386
399
|
aria-label="Regenerate API key"
|
|
400
|
+
data-testid="regenerate-api-key-button"
|
|
387
401
|
>
|
|
388
402
|
<svg
|
|
389
403
|
class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
|
|
@@ -411,12 +425,14 @@
|
|
|
411
425
|
readonly
|
|
412
426
|
class="border-default-300 bg-default-50 w-full rounded-lg border px-3 py-2 pr-10 font-mono text-sm"
|
|
413
427
|
placeholder="No API key generated"
|
|
428
|
+
data-testid="api-key-input"
|
|
414
429
|
/>
|
|
415
430
|
<button
|
|
416
431
|
type="button"
|
|
417
432
|
onclick={() => (showApiKey = !showApiKey)}
|
|
418
433
|
class="text-default-500 hover:text-default-700 absolute top-1/2 right-2 -translate-y-1/2"
|
|
419
434
|
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
|
435
|
+
data-testid="toggle-api-key-visibility"
|
|
420
436
|
>
|
|
421
437
|
{#if showApiKey}
|
|
422
438
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -533,6 +549,7 @@
|
|
|
533
549
|
? 'border-blue-500 bg-blue-50 opacity-75'
|
|
534
550
|
: 'border-blue-500 bg-blue-50'
|
|
535
551
|
: 'border-default-200 hover:border-default-300 bg-white'}"
|
|
552
|
+
data-testid="role-{role.value}"
|
|
536
553
|
>
|
|
537
554
|
<div class="flex items-center justify-between gap-2">
|
|
538
555
|
<div class="min-w-0 flex-1">
|
|
@@ -587,7 +604,13 @@
|
|
|
587
604
|
<div></div>
|
|
588
605
|
{/if}
|
|
589
606
|
<div class="flex gap-3">
|
|
590
|
-
<Button
|
|
607
|
+
<Button
|
|
608
|
+
variant="outline"
|
|
609
|
+
onclick={handleClose}
|
|
610
|
+
disabled={saving}
|
|
611
|
+
type="button"
|
|
612
|
+
data-testid="cancel-button"
|
|
613
|
+
>
|
|
591
614
|
Cancel
|
|
592
615
|
</Button>
|
|
593
616
|
<Button
|
|
@@ -596,6 +619,7 @@
|
|
|
596
619
|
onclick={() => formElement?.requestSubmit()}
|
|
597
620
|
disabled={saving}
|
|
598
621
|
isLoading={saving}
|
|
622
|
+
data-testid="save-user-button"
|
|
599
623
|
>
|
|
600
624
|
{mode === 'create' ? 'Create User' : 'Save Changes'}
|
|
601
625
|
</Button>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"format": "prettier --write .",
|
|
19
19
|
"lint": "prettier --check . && eslint .",
|
|
20
20
|
"test:unit": "vitest",
|
|
21
|
+
"test:e2e": "playwright test",
|
|
22
|
+
"test:e2e:ui": "playwright test --ui",
|
|
23
|
+
"test:e2e:debug": "playwright test --debug",
|
|
24
|
+
"test:e2e:report": "playwright show-report",
|
|
21
25
|
"test": "npm run test:unit -- --run",
|
|
22
26
|
"pub:minor": "git add . && git commit -m \"chore: prepare for publish minor\" && npm version minor && git push --follow-tags && npm publish",
|
|
23
27
|
"pub:patch": "git add . && git commit -m \"chore: prepare for publish patch\" && npm version patch && git push --follow-tags && npm publish",
|