@makolabs/ripple 1.2.3 → 1.2.8
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/README.md +77 -0
- package/dist/adapters/ai/OpenAIAdapter.js +16 -11
- package/dist/adapters/ai/types.d.ts +3 -3
- package/dist/adapters/storage/BaseAdapter.d.ts +1 -1
- package/dist/adapters/storage/BaseAdapter.js +1 -1
- package/dist/adapters/storage/S3Adapter.js +2 -2
- package/dist/ai/AIChatInterface.svelte +32 -34
- package/dist/ai/AIChatInterface.svelte.d.ts +0 -1
- package/dist/ai/AIChatInterfaceTestWrapper.svelte +26 -0
- package/dist/ai/AIChatInterfaceTestWrapper.svelte.d.ts +17 -0
- package/dist/ai/ChatInput.svelte +7 -15
- package/dist/ai/ChatInput.svelte.d.ts +0 -2
- package/dist/ai/CodeRenderer.svelte +25 -12
- package/dist/ai/ComposeDropdown.svelte +17 -14
- package/dist/ai/MermaidRenderer.svelte +21 -17
- package/dist/ai/MermaidRenderer.svelte.d.ts +0 -1
- package/dist/ai/MessageBox.svelte +10 -7
- package/dist/ai/ThinkingDisplay.svelte +67 -43
- package/dist/ai/ai-chat-interface.d.ts +22 -21
- package/dist/ai/ai-chat-interface.js +8 -7
- package/dist/ai/content-detector.js +2 -2
- package/dist/button/ButtonTestWrapper.svelte +10 -0
- package/dist/button/ButtonTestWrapper.svelte.d.ts +7 -0
- package/dist/charts/Chart.svelte +6 -1
- package/dist/config/ai.js +1 -0
- package/dist/drawer/DrawerTestWrapper.svelte +19 -0
- package/dist/drawer/DrawerTestWrapper.svelte.d.ts +9 -0
- package/dist/drawer/drawer.d.ts +19 -18
- package/dist/drawer/drawer.js +7 -6
- package/dist/elements/accordion/Accordion.svelte +1 -1
- package/dist/elements/accordion/Accordion.svelte.d.ts +1 -1
- package/dist/elements/accordion/AccordionTestWrapper.svelte +21 -0
- package/dist/elements/accordion/AccordionTestWrapper.svelte.d.ts +10 -0
- package/dist/elements/badge/Badge.svelte +5 -4
- package/dist/elements/badge/BadgeTestWrapper.svelte +14 -0
- package/dist/elements/badge/BadgeTestWrapper.svelte.d.ts +9 -0
- package/dist/elements/badge/badge.d.ts +40 -39
- package/dist/elements/badge/badge.js +14 -13
- package/dist/elements/dropdown/Dropdown.svelte +0 -1
- package/dist/elements/pagination/Pagination.svelte +20 -26
- package/dist/elements/progress/Progress.svelte +3 -3
- package/dist/elements/timeline/Timeline.svelte +1 -1
- package/dist/file-browser/FileBrowser.svelte +7 -10
- package/dist/filters/CompactFilters.svelte +3 -3
- package/dist/forms/Checkbox.svelte +0 -1
- package/dist/forms/CheckboxTestWrapper.svelte +8 -0
- package/dist/forms/CheckboxTestWrapper.svelte.d.ts +4 -0
- package/dist/forms/DateRange.svelte +186 -198
- package/dist/forms/Form.svelte +1 -0
- package/dist/forms/Input.svelte +14 -5
- package/dist/forms/InputTestWrapper.svelte +8 -0
- package/dist/forms/InputTestWrapper.svelte.d.ts +4 -0
- package/dist/forms/NumberInput.svelte +2 -2
- package/dist/forms/RadioInputs.svelte +1 -1
- package/dist/forms/RadioPill.svelte +1 -1
- package/dist/forms/Slider.svelte +2 -2
- package/dist/forms/Tags.svelte +3 -3
- package/dist/forms/ToggleTestWrapper.svelte +8 -0
- package/dist/forms/ToggleTestWrapper.svelte.d.ts +7 -0
- package/dist/forms/slider.js +1 -1
- package/dist/header/PageHeader.svelte +2 -1
- package/dist/header/breadcrumbs.d.ts +47 -33
- package/dist/header/breadcrumbs.js +12 -11
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -0
- package/dist/layout/activity-list/ActivityList.svelte +9 -11
- package/dist/layout/card/CardTestWrapper.svelte +15 -0
- package/dist/layout/card/CardTestWrapper.svelte.d.ts +7 -0
- package/dist/layout/card/RankedCard.svelte +2 -3
- package/dist/layout/navbar/navbar.d.ts +19 -18
- package/dist/layout/navbar/navbar.js +7 -6
- package/dist/layout/sidebar/NavGroup.svelte +1 -0
- package/dist/layout/table/Cells.svelte +5 -5
- package/dist/layout/table/Table.svelte +8 -8
- package/dist/layout/table/table.d.ts +28 -24
- package/dist/layout/table/table.js +14 -13
- package/dist/modal/Modal.svelte +1 -1
- package/dist/modal/ModalTestWrapper.svelte +20 -0
- package/dist/modal/ModalTestWrapper.svelte.d.ts +8 -0
- package/dist/modal/modal.d.ts +1 -20
- package/dist/pipeline/Pipeline.svelte +29 -17
- package/dist/user-management/README.md +417 -0
- package/dist/user-management/UserManagement.svelte +245 -0
- package/dist/user-management/UserManagement.svelte.d.ts +4 -0
- package/dist/user-management/UserManagementTestWrapper.svelte +33 -0
- package/dist/user-management/UserManagementTestWrapper.svelte.d.ts +13 -0
- package/dist/user-management/UserModal.svelte +285 -0
- package/dist/user-management/UserModal.svelte.d.ts +4 -0
- package/dist/user-management/UserModalTestWrapper.svelte +22 -0
- package/dist/user-management/UserModalTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/UserTable.svelte +219 -0
- package/dist/user-management/UserTable.svelte.d.ts +4 -0
- package/dist/user-management/UserTableTestWrapper.svelte +41 -0
- package/dist/user-management/UserTableTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/UserViewModal.svelte +276 -0
- package/dist/user-management/UserViewModal.svelte.d.ts +4 -0
- package/dist/user-management/UserViewModalTestWrapper.svelte +22 -0
- package/dist/user-management/UserViewModalTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/adapters/UserManagement.remote.d.ts +68 -0
- package/dist/user-management/adapters/UserManagement.remote.js +487 -0
- package/dist/user-management/adapters/index.d.ts +10 -0
- package/dist/user-management/adapters/index.js +12 -0
- package/dist/user-management/adapters/mockUserManagement.d.ts +70 -0
- package/dist/user-management/adapters/mockUserManagement.js +187 -0
- package/dist/user-management/adapters/types.d.ts +24 -0
- package/dist/user-management/adapters/types.js +7 -0
- package/dist/user-management/index.d.ts +12 -0
- package/dist/user-management/index.js +11 -0
- package/dist/user-management/user-management.d.ts +126 -0
- package/dist/user-management/user-management.js +42 -0
- package/package.json +4 -1
- package/dist/types/markdown.d.ts +0 -14
- package/dist/types/variants.d.ts +0 -1
- package/dist/types/variants.js +0 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Remote Functions
|
|
3
|
+
*
|
|
4
|
+
* Complete implementation template for user management remote functions.
|
|
5
|
+
* This follows the same pattern as users.remote.ts and is designed to be
|
|
6
|
+
* used as-is in SvelteKit applications like sharkfin-frontend.
|
|
7
|
+
*
|
|
8
|
+
* Configuration:
|
|
9
|
+
* - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
|
|
10
|
+
* - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
|
|
11
|
+
* - Adjust permission prefix filter via PERMISSION_PREFIX env variable
|
|
12
|
+
*
|
|
13
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
14
|
+
*
|
|
15
|
+
* Usage in your app:
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { UserManagement } from '@makolabs/ripple';
|
|
19
|
+
* import * as adapter from '../UserManagement.remote';
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <UserManagement adapter={adapter} />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { query, command } from '$app/server';
|
|
26
|
+
import { env } from '$env/dynamic/private';
|
|
27
|
+
/**
|
|
28
|
+
* Configuration
|
|
29
|
+
* Override these values or import from your config file
|
|
30
|
+
*/
|
|
31
|
+
// Option 1: Import from your config (recommended)
|
|
32
|
+
// import { ADMIN_CONFIG } from '../../constants/permissions';
|
|
33
|
+
// const CLIENT_ID = ADMIN_CONFIG.CLIENT_ID;
|
|
34
|
+
// Option 2: Use environment variable
|
|
35
|
+
const CLIENT_ID = env.CLIENT_ID || 'sharkfin'; // Default fallback
|
|
36
|
+
// Permission prefix filter - adjust based on your permission structure
|
|
37
|
+
// Example: 'sharkfin:' filters to only sharkfin permissions
|
|
38
|
+
const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
|
|
39
|
+
// Organization ID for adding users to organizations
|
|
40
|
+
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
41
|
+
/**
|
|
42
|
+
* Helper: Make authenticated request to Clerk API
|
|
43
|
+
*/
|
|
44
|
+
async function makeClerkRequest(endpoint, options = {}) {
|
|
45
|
+
const CLERK_SECRET_KEY = env.CLERK_SECRET_KEY;
|
|
46
|
+
if (!CLERK_SECRET_KEY) {
|
|
47
|
+
throw new Error('CLERK_SECRET_KEY environment variable is required');
|
|
48
|
+
}
|
|
49
|
+
const response = await fetch(`https://api.clerk.com/v1${endpoint}`, {
|
|
50
|
+
...options,
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${CLERK_SECRET_KEY}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
...options.headers
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
console.error(`❌ [Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
60
|
+
// Try to parse error details
|
|
61
|
+
let errorDetails;
|
|
62
|
+
try {
|
|
63
|
+
errorDetails = JSON.parse(errorText);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
errorDetails = { message: errorText || `${response.status} ${response.statusText}` };
|
|
67
|
+
}
|
|
68
|
+
// Throw error with status and details for better frontend handling
|
|
69
|
+
const error = new Error(errorDetails.message || `Clerk API request failed: ${response.status} ${response.statusText}`);
|
|
70
|
+
error.status = response.status;
|
|
71
|
+
error.details = errorDetails;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Helper: Make authenticated request to Admin API
|
|
78
|
+
*/
|
|
79
|
+
async function makeAdminRequest(endpoint, options = {}) {
|
|
80
|
+
const ADMIN_API_KEY = env.ADMIN_API_KEY;
|
|
81
|
+
const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
|
|
82
|
+
if (!ADMIN_API_KEY || !PRIVATE_BASE_AUTH_URL) {
|
|
83
|
+
const missing = [];
|
|
84
|
+
if (!ADMIN_API_KEY)
|
|
85
|
+
missing.push('ADMIN_API_KEY');
|
|
86
|
+
if (!PRIVATE_BASE_AUTH_URL)
|
|
87
|
+
missing.push('PRIVATE_BASE_AUTH_URL');
|
|
88
|
+
throw new Error(`Admin API configuration missing: ${missing.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
headers: {
|
|
94
|
+
'X-Admin-API-Key': ADMIN_API_KEY,
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
...options.headers
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
console.error(`❌ [Admin API] ${response.status} ${response.statusText} - ${errorText}`);
|
|
102
|
+
throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
103
|
+
}
|
|
104
|
+
return response.json();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Helper: Create admin key with permissions for a user
|
|
108
|
+
*
|
|
109
|
+
* @param userId - The user ID (used as 'sub' in admin API)
|
|
110
|
+
* @param permissions - Array of permission strings
|
|
111
|
+
* @param clientId - Client ID (defaults to CLIENT_ID config)
|
|
112
|
+
*/
|
|
113
|
+
async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
|
|
114
|
+
console.log(`🔑 [createUserPermissions] Creating admin key for user ${userId}`);
|
|
115
|
+
// Filter permissions by prefix if configured
|
|
116
|
+
const filteredPermissions = PERMISSION_PREFIX
|
|
117
|
+
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
118
|
+
: permissions;
|
|
119
|
+
if (filteredPermissions.length === 0) {
|
|
120
|
+
console.log(`⚠️ [createUserPermissions] No ${PERMISSION_PREFIX || ''} permissions provided, skipping`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const createData = await makeAdminRequest('/admin/keys', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
client_id: clientId,
|
|
127
|
+
sub: userId, // userId is used as 'sub' parameter
|
|
128
|
+
scopes: filteredPermissions
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
console.log(`✅ [createUserPermissions] Admin key created successfully for user ${userId}`);
|
|
132
|
+
return createData;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get users with pagination and sorting
|
|
136
|
+
*
|
|
137
|
+
* Transforms adapter options to Clerk API format
|
|
138
|
+
*/
|
|
139
|
+
export const getUsers = query('unchecked', async (options) => {
|
|
140
|
+
console.log('🔍 [getUsers] Fetching users with options:', options);
|
|
141
|
+
try {
|
|
142
|
+
// Transform adapter options to Clerk API format
|
|
143
|
+
const limit = options.pageSize;
|
|
144
|
+
const offset = (options.page - 1) * options.pageSize;
|
|
145
|
+
// Build orderBy string
|
|
146
|
+
let orderBy = '';
|
|
147
|
+
if (options.sortBy) {
|
|
148
|
+
const prefix = options.sortOrder === 'desc' ? '-' : '';
|
|
149
|
+
orderBy = `${prefix}${options.sortBy}`;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
orderBy = '-created_at'; // Default
|
|
153
|
+
}
|
|
154
|
+
// Build Clerk API parameters
|
|
155
|
+
const params = new URLSearchParams({
|
|
156
|
+
limit: limit.toString(),
|
|
157
|
+
offset: offset.toString(),
|
|
158
|
+
order_by: orderBy
|
|
159
|
+
});
|
|
160
|
+
if (options.query)
|
|
161
|
+
params.append('query', options.query);
|
|
162
|
+
// Fetch users and count in parallel
|
|
163
|
+
const [usersData, countData] = await Promise.all([
|
|
164
|
+
makeClerkRequest(`/users?${params}`),
|
|
165
|
+
makeClerkRequest(`/users/count?${params}`)
|
|
166
|
+
]);
|
|
167
|
+
console.log(`✅ [getUsers] Fetched ${usersData.length} users, total: ${countData.total_count}`);
|
|
168
|
+
return {
|
|
169
|
+
users: usersData,
|
|
170
|
+
totalUsers: countData.total_count || 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error('❌ [getUsers] Error:', error);
|
|
175
|
+
throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* Create a new user
|
|
180
|
+
*
|
|
181
|
+
* Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
|
|
182
|
+
*/
|
|
183
|
+
export const createUser = command('unchecked', async (userData) => {
|
|
184
|
+
// Transform User to Clerk API format
|
|
185
|
+
const emailAddress = userData.email_addresses?.[0]?.email_address || '';
|
|
186
|
+
if (!emailAddress) {
|
|
187
|
+
throw new Error('Email address is required');
|
|
188
|
+
}
|
|
189
|
+
console.log('📝 [createUser] Creating new user:', emailAddress);
|
|
190
|
+
try {
|
|
191
|
+
// Extract permissions from userData
|
|
192
|
+
const { permissions, ...userDataOnly } = userData;
|
|
193
|
+
// Transform userData to match Clerk Backend API format (snake_case)
|
|
194
|
+
const clerkUserData = {
|
|
195
|
+
first_name: userDataOnly.first_name || '',
|
|
196
|
+
last_name: userDataOnly.last_name || '',
|
|
197
|
+
email_address: [emailAddress], // Clerk expects an array
|
|
198
|
+
...(userDataOnly.username && { username: userDataOnly.username }),
|
|
199
|
+
...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
|
|
200
|
+
};
|
|
201
|
+
// Step 1: Create user in Clerk
|
|
202
|
+
console.log('📝 [createUser] Creating user in Clerk...');
|
|
203
|
+
let result = await makeClerkRequest('/users', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: JSON.stringify(clerkUserData)
|
|
206
|
+
});
|
|
207
|
+
// Step 1.5: Add user to organization if configured
|
|
208
|
+
if (ORGANIZATION_ID) {
|
|
209
|
+
try {
|
|
210
|
+
console.log(`🏢 [createUser] Adding user ${result.id} to organization ${ORGANIZATION_ID}...`);
|
|
211
|
+
await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
user_id: result.id,
|
|
215
|
+
role: 'org:member' // Standard Clerk organization role
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
console.log(`✅ [createUser] User added to organization successfully`);
|
|
219
|
+
}
|
|
220
|
+
catch (orgError) {
|
|
221
|
+
console.error(`❌ [createUser] Failed to add user to organization:`, orgError);
|
|
222
|
+
// Clean up: delete the user since they can't be added to the org
|
|
223
|
+
try {
|
|
224
|
+
await makeClerkRequest(`/users/${result.id}`, {
|
|
225
|
+
method: 'DELETE'
|
|
226
|
+
});
|
|
227
|
+
console.log(`🗑️ [createUser] User ${result.id} deleted due to org membership failure`);
|
|
228
|
+
}
|
|
229
|
+
catch (deleteError) {
|
|
230
|
+
console.error(`❌ [createUser] Failed to delete user after org membership failure:`, deleteError);
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.warn('⚠️ [createUser] ALLOWED_ORG_ID not configured, skipping organization membership');
|
|
237
|
+
}
|
|
238
|
+
// Step 2: Create admin key with permissions if permissions are provided
|
|
239
|
+
if (permissions && permissions.length > 0) {
|
|
240
|
+
try {
|
|
241
|
+
console.log(`🔑 [createUser] Creating permissions for new user: ${result.id}`);
|
|
242
|
+
const adminKeyResult = await createUserPermissions(result.id, permissions);
|
|
243
|
+
// Extract the API key from the admin service response
|
|
244
|
+
const apiKey = adminKeyResult?.data?.key;
|
|
245
|
+
if (adminKeyResult && apiKey) {
|
|
246
|
+
// Update user's private metadata with the API key
|
|
247
|
+
const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
private_metadata: {
|
|
251
|
+
...result.private_metadata,
|
|
252
|
+
mako_api_key: apiKey
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
});
|
|
256
|
+
// Update the result with the updated user data
|
|
257
|
+
result = updatedUser;
|
|
258
|
+
console.log(`✅ [createUser] API key stored in user's private metadata`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.warn(`⚠️ [createUser] No API key found in admin key result`);
|
|
262
|
+
}
|
|
263
|
+
// Add permission info to result
|
|
264
|
+
result.adminKey = adminKeyResult;
|
|
265
|
+
result.permissionsAssigned = permissions;
|
|
266
|
+
console.log(`✅ [createUser] User ${result.id} created with permissions successfully`);
|
|
267
|
+
}
|
|
268
|
+
catch (permissionError) {
|
|
269
|
+
console.error(`❌ [createUser] Failed to assign permissions:`, permissionError);
|
|
270
|
+
// Don't fail the entire operation, but warn
|
|
271
|
+
result.warning = 'User created but permissions assignment failed';
|
|
272
|
+
result.permissionError =
|
|
273
|
+
permissionError instanceof Error ? permissionError.message : String(permissionError);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.log(`⚠️ [createUser] User ${result.id} created without permissions`);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error('❌ [createUser] Error:', error);
|
|
283
|
+
// Handle Clerk API errors with detailed information
|
|
284
|
+
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
285
|
+
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
286
|
+
enrichedError.status = error.status;
|
|
287
|
+
enrichedError.details = error.details;
|
|
288
|
+
enrichedError.clerkError = true;
|
|
289
|
+
throw enrichedError;
|
|
290
|
+
}
|
|
291
|
+
throw new Error(`Failed to create user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
/**
|
|
295
|
+
* Update an existing user
|
|
296
|
+
*/
|
|
297
|
+
export const updateUser = command('unchecked', async (options) => {
|
|
298
|
+
const { userId, userData } = options;
|
|
299
|
+
console.log(`📝 [updateUser] Updating user ${userId}`);
|
|
300
|
+
try {
|
|
301
|
+
// Transform User to Clerk API format
|
|
302
|
+
// Only include fields that Clerk accepts
|
|
303
|
+
const updateData = {};
|
|
304
|
+
if (userData.first_name !== undefined)
|
|
305
|
+
updateData.first_name = userData.first_name;
|
|
306
|
+
if (userData.last_name !== undefined)
|
|
307
|
+
updateData.last_name = userData.last_name;
|
|
308
|
+
if (userData.username !== undefined && userData.username !== '') {
|
|
309
|
+
// Only include username if it's not empty (prevents "username already exists" error)
|
|
310
|
+
updateData.username = userData.username;
|
|
311
|
+
}
|
|
312
|
+
if (userData.private_metadata !== undefined) {
|
|
313
|
+
updateData.private_metadata = userData.private_metadata;
|
|
314
|
+
}
|
|
315
|
+
const result = await makeClerkRequest(`/users/${userId}`, {
|
|
316
|
+
method: 'PATCH',
|
|
317
|
+
body: JSON.stringify(updateData)
|
|
318
|
+
});
|
|
319
|
+
// If permissions changed, update them separately
|
|
320
|
+
if (userData.permissions !== undefined) {
|
|
321
|
+
try {
|
|
322
|
+
await updateUserPermissions({ userId, permissions: userData.permissions });
|
|
323
|
+
}
|
|
324
|
+
catch (permError) {
|
|
325
|
+
console.error(`❌ [updateUser] Failed to update permissions:`, permError);
|
|
326
|
+
// Don't fail the entire operation
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log(`✅ [updateUser] User ${userId} updated successfully`);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('❌ [updateUser] Error:', error);
|
|
334
|
+
// Handle Clerk API errors with detailed information
|
|
335
|
+
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
336
|
+
const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
|
|
337
|
+
enrichedError.status = error.status;
|
|
338
|
+
enrichedError.details = error.details;
|
|
339
|
+
enrichedError.clerkError = true;
|
|
340
|
+
throw enrichedError;
|
|
341
|
+
}
|
|
342
|
+
throw new Error(`Failed to update user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
/**
|
|
346
|
+
* Delete a single user
|
|
347
|
+
*/
|
|
348
|
+
export const deleteUser = command('unchecked', async (userId) => {
|
|
349
|
+
console.log(`🗑️ [deleteUser] Deleting user ${userId}`);
|
|
350
|
+
try {
|
|
351
|
+
await makeClerkRequest(`/users/${userId}`, {
|
|
352
|
+
method: 'DELETE'
|
|
353
|
+
});
|
|
354
|
+
console.log(`✅ [deleteUser] User ${userId} deleted successfully`);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('❌ [deleteUser] Error:', error);
|
|
358
|
+
throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
/**
|
|
362
|
+
* Delete multiple users
|
|
363
|
+
*/
|
|
364
|
+
export const deleteUsers = command('unchecked', async (userIds) => {
|
|
365
|
+
console.log(`🗑️ [deleteUsers] Deleting ${userIds.length} users`);
|
|
366
|
+
try {
|
|
367
|
+
await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
|
|
368
|
+
method: 'DELETE'
|
|
369
|
+
})));
|
|
370
|
+
console.log(`✅ [deleteUsers] ${userIds.length} users deleted successfully`);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
console.error('❌ [deleteUsers] Error:', error);
|
|
374
|
+
throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
/**
|
|
378
|
+
* Get permissions for a specific user
|
|
379
|
+
*
|
|
380
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
381
|
+
*/
|
|
382
|
+
export const getUserPermissions = query('unchecked', async (userId) => {
|
|
383
|
+
console.log(`🔍 [getUserPermissions] Fetching permissions for user: ${userId}`);
|
|
384
|
+
try {
|
|
385
|
+
// Try direct lookup first using client_id and sub (userId)
|
|
386
|
+
try {
|
|
387
|
+
const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
388
|
+
console.log(`✅ [getUserPermissions] Direct lookup successful for user ${userId}`);
|
|
389
|
+
// Filter the response to only include active keys
|
|
390
|
+
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
391
|
+
userData.data.data = userData.data.data.filter((key) => key.status === 'active');
|
|
392
|
+
console.log(`🔍 [getUserPermissions] Filtered to ${userData.data.data.length} active key(s)`);
|
|
393
|
+
}
|
|
394
|
+
// Extract scopes from the response
|
|
395
|
+
if (userData?.data?.data && Array.isArray(userData.data.data)) {
|
|
396
|
+
return userData.data.data.flatMap((key) => key.scopes || []);
|
|
397
|
+
}
|
|
398
|
+
else if (userData?.scopes) {
|
|
399
|
+
return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
console.log(`❌ [getUserPermissions] Direct lookup failed, trying search by sub field`);
|
|
405
|
+
// If direct lookup fails with 404, search all keys by sub field
|
|
406
|
+
try {
|
|
407
|
+
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
408
|
+
console.log(`🔍 [getUserPermissions] Searching through ${allKeysData.data?.data?.length || 0} keys`);
|
|
409
|
+
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
410
|
+
// Match by sub (userId) and client_id
|
|
411
|
+
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
412
|
+
if (userKey) {
|
|
413
|
+
console.log(`✅ [getUserPermissions] Found active user key by sub field`);
|
|
414
|
+
return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log(`❌ [getUserPermissions] No user found, returning empty permissions`);
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (searchError) {
|
|
422
|
+
console.error('❌ [getUserPermissions] Error searching for user by sub:', searchError);
|
|
423
|
+
throw new Error('Failed to fetch user permissions');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
console.error('❌ [getUserPermissions] Error:', error);
|
|
429
|
+
throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* Update permissions for a specific user
|
|
434
|
+
*
|
|
435
|
+
* Uses 'sub' (userId) parameter in admin API calls
|
|
436
|
+
*/
|
|
437
|
+
export const updateUserPermissions = command('unchecked', async (options) => {
|
|
438
|
+
const { userId, permissions } = options;
|
|
439
|
+
console.log(`🔑 [updateUserPermissions] Updating permissions for user ${userId}`);
|
|
440
|
+
try {
|
|
441
|
+
let adminKeyId = userId; // Use userId as default key ID
|
|
442
|
+
// Try direct update first
|
|
443
|
+
try {
|
|
444
|
+
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
445
|
+
method: 'PUT',
|
|
446
|
+
body: JSON.stringify({ scopes: permissions })
|
|
447
|
+
});
|
|
448
|
+
console.log(`✅ [updateUserPermissions] Permissions updated successfully`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// If direct update fails, try to find the correct adminKeyId
|
|
453
|
+
console.log(`⚠️ [updateUserPermissions] Direct update failed, searching for key ID...`);
|
|
454
|
+
try {
|
|
455
|
+
const allKeysData = await makeAdminRequest('/admin/keys');
|
|
456
|
+
// Find the ACTIVE key for this user (ignore revoked keys)
|
|
457
|
+
// Match by sub (userId) and client_id
|
|
458
|
+
const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
|
|
459
|
+
if (userKey) {
|
|
460
|
+
// Use the found key ID for update
|
|
461
|
+
adminKeyId = userKey.id;
|
|
462
|
+
await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
|
|
463
|
+
method: 'PUT',
|
|
464
|
+
body: JSON.stringify({ scopes: permissions })
|
|
465
|
+
});
|
|
466
|
+
console.log(`✅ [updateUserPermissions] Permissions updated successfully (after search)`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// User doesn't exist, create new admin key
|
|
471
|
+
console.log(`📝 [updateUserPermissions] Creating new admin key...`);
|
|
472
|
+
await createUserPermissions(userId, permissions);
|
|
473
|
+
console.log(`✅ [updateUserPermissions] New admin key created successfully`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (searchError) {
|
|
478
|
+
console.error('❌ [updateUserPermissions] Error during permission update:', searchError);
|
|
479
|
+
throw new Error('Failed to update permissions');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error('❌ [updateUserPermissions] Error:', error);
|
|
485
|
+
throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Adapters
|
|
3
|
+
*
|
|
4
|
+
* Adapters are remote function modules (.remote.ts files) that export
|
|
5
|
+
* query/command functions.
|
|
6
|
+
*
|
|
7
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
|
+
*/
|
|
9
|
+
export type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
10
|
+
export * from './mockUserManagement.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Management Adapters
|
|
3
|
+
*
|
|
4
|
+
* Adapters are remote function modules (.remote.ts files) that export
|
|
5
|
+
* query/command functions.
|
|
6
|
+
*
|
|
7
|
+
* @see https://svelte.dev/docs/kit/remote-functions
|
|
8
|
+
*/
|
|
9
|
+
// Export mock adapter functions for testing/storybook
|
|
10
|
+
export * from './mockUserManagement.js';
|
|
11
|
+
// Note: UserManagement.remote.ts is a template file showing how to implement
|
|
12
|
+
// remote functions in your SvelteKit app. It is not exported from this library.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock User Management Functions
|
|
3
|
+
*
|
|
4
|
+
* Mock implementation of user management functions for testing and development.
|
|
5
|
+
* These are regular async functions that match the UserManagementAdapter interface.
|
|
6
|
+
* They store data in memory.
|
|
7
|
+
*
|
|
8
|
+
* Note: This file uses .remote.ts extension for naming consistency, but these are
|
|
9
|
+
* NOT actual SvelteKit remote functions. They are regular async functions that
|
|
10
|
+
* work in both test and library contexts.
|
|
11
|
+
*
|
|
12
|
+
* For actual remote functions in your app, see UserManagement.remote.ts template.
|
|
13
|
+
*
|
|
14
|
+
* To set initial data, use the resetState function:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { resetState } from './mockUserManagement.js';
|
|
17
|
+
* await resetState({ initialUsers: [...], simulateDelay: false });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import type { User } from '../user-management.js';
|
|
21
|
+
import type { GetUsersOptions, GetUsersResult } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Reset mock adapter state
|
|
24
|
+
*/
|
|
25
|
+
export declare function resetState(options?: {
|
|
26
|
+
initialUsers?: User[];
|
|
27
|
+
simulateDelay?: boolean;
|
|
28
|
+
delayMs?: number;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get users with pagination and sorting
|
|
32
|
+
* Matches UserManagementAdapter.getUsers signature
|
|
33
|
+
*/
|
|
34
|
+
export declare function getUsers(options: GetUsersOptions): Promise<GetUsersResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Create a new user
|
|
37
|
+
* Matches UserManagementAdapter.createUser signature
|
|
38
|
+
*/
|
|
39
|
+
export declare function createUser(userData: Partial<User>): Promise<User>;
|
|
40
|
+
/**
|
|
41
|
+
* Update an existing user
|
|
42
|
+
* Matches UserManagementAdapter.updateUser signature
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateUser(options: {
|
|
45
|
+
userId: string;
|
|
46
|
+
userData: Partial<User>;
|
|
47
|
+
}): Promise<User>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete a single user
|
|
50
|
+
* Matches UserManagementAdapter.deleteUser signature
|
|
51
|
+
*/
|
|
52
|
+
export declare function deleteUser(userId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Delete multiple users
|
|
55
|
+
* Matches UserManagementAdapter.deleteUsers signature
|
|
56
|
+
*/
|
|
57
|
+
export declare function deleteUsers(userIds: string[]): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get permissions for a specific user
|
|
60
|
+
* Matches UserManagementAdapter.getUserPermissions signature
|
|
61
|
+
*/
|
|
62
|
+
export declare function getUserPermissions(userId: string): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Update permissions for a specific user
|
|
65
|
+
* Matches UserManagementAdapter.updateUserPermissions signature
|
|
66
|
+
*/
|
|
67
|
+
export declare function updateUserPermissions(options: {
|
|
68
|
+
userId: string;
|
|
69
|
+
permissions: string[];
|
|
70
|
+
}): Promise<void>;
|