@donotdev/functions 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -7
- package/src/firebase/auth/setCustomClaims.ts +26 -4
- package/src/firebase/baseFunction.ts +43 -20
- package/src/firebase/billing/cancelSubscription.ts +9 -1
- package/src/firebase/billing/changePlan.ts +8 -2
- package/src/firebase/billing/createCustomerPortal.ts +16 -2
- package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
- package/src/firebase/billing/webhookHandler.ts +13 -1
- package/src/firebase/config/constants.ts +0 -3
- package/src/firebase/crud/aggregate.ts +20 -5
- package/src/firebase/crud/create.ts +31 -7
- package/src/firebase/crud/get.ts +16 -8
- package/src/firebase/crud/list.ts +70 -29
- package/src/firebase/crud/update.ts +29 -7
- package/src/firebase/oauth/exchangeToken.ts +30 -4
- package/src/firebase/oauth/githubAccess.ts +8 -3
- package/src/firebase/registerCrudFunctions.ts +15 -4
- package/src/firebase/scheduled/checkExpiredSubscriptions.ts +20 -3
- package/src/shared/__tests__/detectFirestore.test.ts +52 -0
- package/src/shared/__tests__/errorHandling.test.ts +144 -0
- package/src/shared/__tests__/idempotency.test.ts +95 -0
- package/src/shared/__tests__/rateLimiter.test.ts +142 -0
- package/src/shared/__tests__/validation.test.ts +172 -0
- package/src/shared/billing/__tests__/createCheckout.test.ts +393 -0
- package/src/shared/billing/__tests__/webhookHandler.test.ts +1091 -0
- package/src/shared/billing/webhookHandler.ts +16 -7
- package/src/shared/errorHandling.ts +16 -54
- package/src/shared/firebase.ts +1 -25
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
- package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
- package/src/shared/schema.ts +7 -1
- package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
- package/src/shared/utils/external/subscription.ts +10 -0
- package/src/shared/utils/internal/auth.ts +140 -16
- package/src/shared/utils/internal/rateLimiter.ts +101 -90
- package/src/shared/utils/internal/validation.ts +47 -3
- package/src/shared/utils.ts +154 -39
- package/src/supabase/auth/deleteAccount.ts +59 -0
- package/src/supabase/auth/getCustomClaims.ts +56 -0
- package/src/supabase/auth/getUserAuthStatus.ts +64 -0
- package/src/supabase/auth/removeCustomClaims.ts +75 -0
- package/src/supabase/auth/setCustomClaims.ts +73 -0
- package/src/supabase/baseFunction.ts +302 -0
- package/src/supabase/billing/cancelSubscription.ts +57 -0
- package/src/supabase/billing/changePlan.ts +62 -0
- package/src/supabase/billing/createCheckoutSession.ts +82 -0
- package/src/supabase/billing/createCustomerPortal.ts +58 -0
- package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
- package/src/supabase/crud/aggregate.ts +169 -0
- package/src/supabase/crud/create.ts +225 -0
- package/src/supabase/crud/delete.ts +154 -0
- package/src/supabase/crud/get.ts +89 -0
- package/src/supabase/crud/index.ts +24 -0
- package/src/supabase/crud/list.ts +357 -0
- package/src/supabase/crud/update.ts +199 -0
- package/src/supabase/helpers/authProvider.ts +45 -0
- package/src/supabase/index.ts +73 -0
- package/src/supabase/registerCrudFunctions.ts +180 -0
- package/src/supabase/utils/idempotency.ts +141 -0
- package/src/supabase/utils/monitoring.ts +187 -0
- package/src/supabase/utils/rateLimiter.ts +216 -0
- package/src/vercel/api/auth/get-custom-claims.ts +3 -2
- package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
- package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
- package/src/vercel/api/auth/set-custom-claims.ts +5 -2
- package/src/vercel/api/billing/cancel.ts +2 -1
- package/src/vercel/api/billing/change-plan.ts +3 -1
- package/src/vercel/api/billing/customer-portal.ts +4 -1
- package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
- package/src/vercel/api/billing/webhook-handler.ts +24 -4
- package/src/vercel/api/crud/create.ts +14 -8
- package/src/vercel/api/crud/delete.ts +15 -6
- package/src/vercel/api/crud/get.ts +16 -8
- package/src/vercel/api/crud/list.ts +22 -10
- package/src/vercel/api/crud/update.ts +16 -10
- package/src/vercel/api/oauth/check-github-access.ts +2 -5
- package/src/vercel/api/oauth/grant-github-access.ts +1 -5
- package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
- package/src/vercel/api/utils/cors.ts +13 -2
- package/src/vercel/baseFunction.ts +40 -25
package/src/shared/utils.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import Stripe from 'stripe';
|
|
13
|
+
import * as v from 'valibot';
|
|
13
14
|
|
|
14
15
|
import { DoNotDevError } from '@donotdev/core/server';
|
|
15
16
|
import type { OAuthPartnerId, UserRole } from '@donotdev/core/server';
|
|
@@ -101,9 +102,13 @@ export const stripe = new Proxy({} as Stripe, {
|
|
|
101
102
|
});
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
|
-
* Assert that a user is authenticated
|
|
105
|
+
* Assert that a user is authenticated from a Firebase callable auth context.
|
|
106
|
+
* For Vercel/Next.js routes, use `verifyAuthToken` from `shared/utils/internal/auth.js` instead.
|
|
105
107
|
*
|
|
106
|
-
* @
|
|
108
|
+
* @param auth - Firebase callable request auth context (object with `.uid`)
|
|
109
|
+
* @returns The authenticated user's uid
|
|
110
|
+
*
|
|
111
|
+
* @version 0.0.2
|
|
107
112
|
* @since 0.0.1
|
|
108
113
|
* @author AMBROISE PARK Consulting
|
|
109
114
|
*/
|
|
@@ -220,68 +225,91 @@ export function createErrorResponse(error: unknown): {
|
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
/**
|
|
223
|
-
* Updates user subscription in Firebase
|
|
224
|
-
* TODO: Implement this function based on your Firebase setup
|
|
228
|
+
* Updates user subscription in Firebase.
|
|
225
229
|
*
|
|
226
|
-
*
|
|
230
|
+
* **Architecture decision — throwing stubs for billing functions:**
|
|
231
|
+
* These subscription functions (`updateUserSubscription`, `getUserSubscription`,
|
|
232
|
+
* `cancelUserSubscription`, `handleSubscriptionCancellation`) are intentional
|
|
233
|
+
* placeholder implementations. They exist so the framework compiles and
|
|
234
|
+
* type-checks out of the box, but throw at runtime to surface missing
|
|
235
|
+
* integration early. Consumer apps replace them with their billing provider
|
|
236
|
+
* integration (Stripe, Paddle, LemonSqueezy, etc.) via the documented
|
|
237
|
+
* `shared/billing/helpers/` modules.
|
|
238
|
+
*
|
|
239
|
+
* C10: This was a silent no-op stub. It now throws to surface the missing
|
|
240
|
+
* implementation at startup rather than silently dropping webhook events.
|
|
241
|
+
* Use `updateUserSubscription` from `shared/billing/helpers/updateUserSubscription.ts`
|
|
242
|
+
* (which requires an authProvider) instead.
|
|
243
|
+
*
|
|
244
|
+
* @deprecated Use shared/billing/helpers/updateUserSubscription.ts
|
|
245
|
+
* @version 0.0.2
|
|
227
246
|
* @since 0.0.1
|
|
228
247
|
* @author AMBROISE PARK Consulting
|
|
229
248
|
*/
|
|
230
249
|
export async function updateUserSubscription(
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
_firebaseUid: string,
|
|
251
|
+
_subscription: any
|
|
233
252
|
): Promise<void> {
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
throw new DoNotDevError(
|
|
254
|
+
'updateUserSubscription stub called — import from shared/billing/helpers/updateUserSubscription.ts and supply an authProvider',
|
|
255
|
+
'unimplemented'
|
|
256
|
+
);
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
/**
|
|
239
|
-
* Gets user subscription from Firebase
|
|
240
|
-
* TODO: Implement this function based on your Firebase setup
|
|
260
|
+
* Gets user subscription from Firebase.
|
|
241
261
|
*
|
|
242
|
-
*
|
|
262
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
263
|
+
*
|
|
264
|
+
* @deprecated Implement directly using Firebase Admin SDK or Stripe API.
|
|
265
|
+
* @version 0.0.2
|
|
243
266
|
* @since 0.0.1
|
|
244
267
|
* @author AMBROISE PARK Consulting
|
|
245
268
|
*/
|
|
246
|
-
export async function getUserSubscription(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
export async function getUserSubscription(_firebaseUid: string): Promise<any> {
|
|
270
|
+
throw new DoNotDevError(
|
|
271
|
+
'getUserSubscription stub called — implement using Firebase Admin SDK or Stripe API',
|
|
272
|
+
'unimplemented'
|
|
273
|
+
);
|
|
250
274
|
}
|
|
251
275
|
|
|
252
276
|
/**
|
|
253
|
-
* Cancels user subscription
|
|
254
|
-
* TODO: Implement this function based on your Firebase setup
|
|
277
|
+
* Cancels user subscription.
|
|
255
278
|
*
|
|
256
|
-
*
|
|
279
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
280
|
+
*
|
|
281
|
+
* @deprecated Use cancelUserSubscription from shared/billing/helpers/subscriptionManagement.ts
|
|
282
|
+
* @version 0.0.2
|
|
257
283
|
* @since 0.0.1
|
|
258
284
|
* @author AMBROISE PARK Consulting
|
|
259
285
|
*/
|
|
260
286
|
export async function cancelUserSubscription(
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
_firebaseUid: string,
|
|
288
|
+
_subscription: any
|
|
263
289
|
): Promise<void> {
|
|
264
|
-
|
|
265
|
-
|
|
290
|
+
throw new DoNotDevError(
|
|
291
|
+
'cancelUserSubscription stub called — import from shared/billing/helpers/subscriptionManagement.ts',
|
|
292
|
+
'unimplemented'
|
|
293
|
+
);
|
|
266
294
|
}
|
|
267
295
|
|
|
268
296
|
/**
|
|
269
|
-
* Handles subscription cancellation
|
|
270
|
-
* TODO: Implement this function based on your Firebase setup
|
|
297
|
+
* Handles subscription cancellation.
|
|
271
298
|
*
|
|
272
|
-
*
|
|
299
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
300
|
+
*
|
|
301
|
+
* @deprecated Implement using Stripe webhook events and updateUserSubscription.
|
|
302
|
+
* @version 0.0.2
|
|
273
303
|
* @since 0.0.1
|
|
274
304
|
* @author AMBROISE PARK Consulting
|
|
275
305
|
*/
|
|
276
306
|
export async function handleSubscriptionCancellation(
|
|
277
|
-
|
|
278
|
-
|
|
307
|
+
_firebaseUid: string,
|
|
308
|
+
_subscription: any
|
|
279
309
|
): Promise<void> {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'
|
|
283
|
-
firebaseUid,
|
|
284
|
-
subscription.id
|
|
310
|
+
throw new DoNotDevError(
|
|
311
|
+
'handleSubscriptionCancellation stub called — handle via Stripe webhook events',
|
|
312
|
+
'unimplemented'
|
|
285
313
|
);
|
|
286
314
|
}
|
|
287
315
|
|
|
@@ -335,6 +363,54 @@ export function validateDocument(data: any, schema?: any): void {
|
|
|
335
363
|
if (Array.isArray(data)) {
|
|
336
364
|
throw new Error('Document data cannot be an array');
|
|
337
365
|
}
|
|
366
|
+
|
|
367
|
+
// Run Valibot schema validation when a schema is provided
|
|
368
|
+
if (schema) {
|
|
369
|
+
try {
|
|
370
|
+
v.parse(schema, data);
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
if (error?.issues) {
|
|
373
|
+
const messages = error.issues
|
|
374
|
+
.map(
|
|
375
|
+
(issue: v.BaseIssue<unknown>) =>
|
|
376
|
+
`${issue.path?.map((p: any) => p.key).join('.') || 'root'}: ${issue.message}`
|
|
377
|
+
)
|
|
378
|
+
.join('; ');
|
|
379
|
+
throw new DoNotDevError(
|
|
380
|
+
`Validation failed: ${messages}`,
|
|
381
|
+
'invalid-argument',
|
|
382
|
+
{ details: { validationErrors: error.issues } }
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Validates a Firestore collection name from client-supplied schema.
|
|
392
|
+
*
|
|
393
|
+
* W22: Vercel CRUD handlers accept `schema` (including collection name) from
|
|
394
|
+
* the client. This is a known design limitation. As a defense-in-depth measure,
|
|
395
|
+
* reject collection names that could be used for path traversal or access to
|
|
396
|
+
* internal collections.
|
|
397
|
+
*
|
|
398
|
+
* @param name - Collection name to validate
|
|
399
|
+
* @throws Error if the name is unsafe
|
|
400
|
+
*
|
|
401
|
+
* @version 0.0.1
|
|
402
|
+
* @since 0.0.1
|
|
403
|
+
* @author AMBROISE PARK Consulting
|
|
404
|
+
*/
|
|
405
|
+
export function validateCollectionName(name: string): void {
|
|
406
|
+
if (!name || typeof name !== 'string') {
|
|
407
|
+
throw new Error('Collection name is required');
|
|
408
|
+
}
|
|
409
|
+
if (name.includes('/') || name.includes('..') || name.startsWith('_')) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
'Invalid collection name: must not contain "/", "..", or start with "_"'
|
|
412
|
+
);
|
|
413
|
+
}
|
|
338
414
|
}
|
|
339
415
|
|
|
340
416
|
/**
|
|
@@ -345,9 +421,16 @@ export function validateDocument(data: any, schema?: any): void {
|
|
|
345
421
|
* @author AMBROISE PARK Consulting
|
|
346
422
|
*/
|
|
347
423
|
export async function verifyFirebaseAuthToken(token: string): Promise<string> {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
424
|
+
if (!token) {
|
|
425
|
+
throw new DoNotDevError('Missing authentication token', 'unauthenticated');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const decodedToken = await getFirebaseAdminAuth().verifyIdToken(token);
|
|
430
|
+
return decodedToken.uid;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new DoNotDevError('Invalid or expired token', 'unauthenticated');
|
|
433
|
+
}
|
|
351
434
|
}
|
|
352
435
|
|
|
353
436
|
/**
|
|
@@ -357,7 +440,39 @@ export async function verifyFirebaseAuthToken(token: string): Promise<string> {
|
|
|
357
440
|
* @since 0.0.1
|
|
358
441
|
* @author AMBROISE PARK Consulting
|
|
359
442
|
*/
|
|
360
|
-
export function findReferences(
|
|
361
|
-
|
|
362
|
-
|
|
443
|
+
export async function findReferences(
|
|
444
|
+
collection: string,
|
|
445
|
+
docId: string,
|
|
446
|
+
referenceMetadata?: {
|
|
447
|
+
incoming?: Array<{
|
|
448
|
+
sourceCollection: string;
|
|
449
|
+
sourceField: string;
|
|
450
|
+
}>;
|
|
451
|
+
}
|
|
452
|
+
): Promise<Array<{ collection: string; field: string; count: number }>> {
|
|
453
|
+
const references: Array<{ collection: string; field: string; count: number }> = [];
|
|
454
|
+
|
|
455
|
+
if (!referenceMetadata?.incoming?.length) {
|
|
456
|
+
return references;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const db = getFirebaseAdminFirestore();
|
|
460
|
+
|
|
461
|
+
for (const ref of referenceMetadata.incoming) {
|
|
462
|
+
const snapshot = await db
|
|
463
|
+
.collection(ref.sourceCollection)
|
|
464
|
+
.where(ref.sourceField, '==', docId)
|
|
465
|
+
.limit(1)
|
|
466
|
+
.get();
|
|
467
|
+
|
|
468
|
+
if (!snapshot.empty) {
|
|
469
|
+
references.push({
|
|
470
|
+
collection: ref.sourceCollection,
|
|
471
|
+
field: ref.sourceField,
|
|
472
|
+
count: snapshot.size,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return references;
|
|
363
478
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// packages/functions/src/supabase/auth/deleteAccount.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Delete Account Supabase Edge Function
|
|
5
|
+
* @description Deletes the authenticated user's account via Supabase Admin.
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.5.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from 'valibot';
|
|
13
|
+
|
|
14
|
+
import { createSupabaseHandler } from '../baseFunction.js';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schema
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const deleteAccountSchema = v.object({
|
|
21
|
+
userId: v.pipe(v.string(), v.minLength(1, 'User ID is required')),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Handler
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a Supabase Edge Function handler for account deletion.
|
|
30
|
+
*
|
|
31
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // supabase/functions/delete-account/index.ts
|
|
36
|
+
* import { createDeleteAccount } from '@donotdev/functions/supabase';
|
|
37
|
+
* Deno.serve(createDeleteAccount());
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @version 0.0.1
|
|
41
|
+
* @since 0.5.0
|
|
42
|
+
*/
|
|
43
|
+
export function createDeleteAccount() {
|
|
44
|
+
return createSupabaseHandler(
|
|
45
|
+
'delete-account',
|
|
46
|
+
deleteAccountSchema,
|
|
47
|
+
async (data, ctx) => {
|
|
48
|
+
// Guard: users can only delete their own account
|
|
49
|
+
if (data.userId !== ctx.uid) {
|
|
50
|
+
throw new Error('Forbidden: cannot delete another user');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { error } = await ctx.supabaseAdmin.auth.admin.deleteUser(data.userId);
|
|
54
|
+
if (error) throw error;
|
|
55
|
+
|
|
56
|
+
return { success: true };
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// packages/functions/src/supabase/auth/getCustomClaims.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Get Custom Claims Supabase Edge Function
|
|
5
|
+
* @description Returns the authenticated user's app_metadata (custom claims).
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.6.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from 'valibot';
|
|
13
|
+
|
|
14
|
+
import { createSupabaseHandler } from '../baseFunction.js';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schema
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const getCustomClaimsSchema = v.object({});
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Handler
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a Supabase Edge Function handler for retrieving custom claims (app_metadata).
|
|
28
|
+
*
|
|
29
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // supabase/functions/get-custom-claims/index.ts
|
|
34
|
+
* import { createGetCustomClaims } from '@donotdev/functions/supabase';
|
|
35
|
+
* Deno.serve(createGetCustomClaims());
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @version 0.0.1
|
|
39
|
+
* @since 0.6.0
|
|
40
|
+
*/
|
|
41
|
+
export function createGetCustomClaims() {
|
|
42
|
+
return createSupabaseHandler(
|
|
43
|
+
'get-custom-claims',
|
|
44
|
+
getCustomClaimsSchema,
|
|
45
|
+
async (_data, ctx) => {
|
|
46
|
+
const { data: { user }, error } =
|
|
47
|
+
await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
|
|
48
|
+
if (error || !user) {
|
|
49
|
+
throw new Error('User not found');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { customClaims: user.app_metadata ?? {} };
|
|
53
|
+
},
|
|
54
|
+
'user',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// packages/functions/src/supabase/auth/getUserAuthStatus.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Get User Auth Status Supabase Edge Function
|
|
5
|
+
* @description Returns the authenticated user's profile and custom claims.
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.6.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from 'valibot';
|
|
13
|
+
|
|
14
|
+
import { createSupabaseHandler } from '../baseFunction.js';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schema
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const getUserAuthStatusSchema = v.object({});
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Handler
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a Supabase Edge Function handler for retrieving user auth status.
|
|
28
|
+
*
|
|
29
|
+
* Returns uid, email, verification status, custom claims, and ban status.
|
|
30
|
+
*
|
|
31
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // supabase/functions/get-user-auth-status/index.ts
|
|
36
|
+
* import { createGetUserAuthStatus } from '@donotdev/functions/supabase';
|
|
37
|
+
* Deno.serve(createGetUserAuthStatus());
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @version 0.0.1
|
|
41
|
+
* @since 0.6.0
|
|
42
|
+
*/
|
|
43
|
+
export function createGetUserAuthStatus() {
|
|
44
|
+
return createSupabaseHandler(
|
|
45
|
+
'get-user-auth-status',
|
|
46
|
+
getUserAuthStatusSchema,
|
|
47
|
+
async (_data, ctx) => {
|
|
48
|
+
const { data: { user }, error } =
|
|
49
|
+
await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
|
|
50
|
+
if (error || !user) {
|
|
51
|
+
throw new Error('User not found');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
uid: user.id,
|
|
56
|
+
email: user.email,
|
|
57
|
+
emailVerified: user.email_confirmed_at != null,
|
|
58
|
+
customClaims: user.app_metadata ?? {},
|
|
59
|
+
disabled: user.banned_until != null,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
'user',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// packages/functions/src/supabase/auth/removeCustomClaims.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Remove Custom Claims Supabase Edge Function
|
|
5
|
+
* @description Removes specific keys from the user's app_metadata.
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.6.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from 'valibot';
|
|
13
|
+
|
|
14
|
+
import { createSupabaseHandler } from '../baseFunction.js';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schema
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const removeCustomClaimsSchema = v.object({
|
|
21
|
+
claimsToRemove: v.pipe(
|
|
22
|
+
v.array(v.string()),
|
|
23
|
+
v.minLength(1, 'At least one claim key is required'),
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Handler
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a Supabase Edge Function handler for removing custom claims from app_metadata.
|
|
33
|
+
*
|
|
34
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // supabase/functions/remove-custom-claims/index.ts
|
|
39
|
+
* import { createRemoveCustomClaims } from '@donotdev/functions/supabase';
|
|
40
|
+
* Deno.serve(createRemoveCustomClaims());
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @version 0.0.1
|
|
44
|
+
* @since 0.6.0
|
|
45
|
+
*/
|
|
46
|
+
export function createRemoveCustomClaims() {
|
|
47
|
+
return createSupabaseHandler(
|
|
48
|
+
'remove-custom-claims',
|
|
49
|
+
removeCustomClaimsSchema,
|
|
50
|
+
async (data, ctx) => {
|
|
51
|
+
// Get current app_metadata
|
|
52
|
+
const { data: { user }, error: getUserError } =
|
|
53
|
+
await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
|
|
54
|
+
if (getUserError || !user) {
|
|
55
|
+
throw new Error('User not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Remove specified keys
|
|
59
|
+
const existingClaims = { ...(user.app_metadata ?? {}) } as Record<string, unknown>;
|
|
60
|
+
for (const key of data.claimsToRemove) {
|
|
61
|
+
delete existingClaims[key];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Update app_metadata
|
|
65
|
+
const { error: updateError } =
|
|
66
|
+
await ctx.supabaseAdmin.auth.admin.updateUserById(ctx.uid, {
|
|
67
|
+
app_metadata: existingClaims,
|
|
68
|
+
});
|
|
69
|
+
if (updateError) throw updateError;
|
|
70
|
+
|
|
71
|
+
return { success: true, customClaims: existingClaims };
|
|
72
|
+
},
|
|
73
|
+
'user',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// packages/functions/src/supabase/auth/setCustomClaims.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Set Custom Claims Supabase Edge Function
|
|
5
|
+
* @description Merges custom claims into the user's app_metadata.
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.6.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from 'valibot';
|
|
13
|
+
|
|
14
|
+
import { createSupabaseHandler } from '../baseFunction.js';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schema
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const setCustomClaimsSchema = v.object({
|
|
21
|
+
customClaims: v.record(v.string(), v.unknown()),
|
|
22
|
+
idempotencyKey: v.optional(v.string()),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Handler
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a Supabase Edge Function handler for setting custom claims (app_metadata).
|
|
31
|
+
*
|
|
32
|
+
* Merges provided claims into the user's existing app_metadata.
|
|
33
|
+
*
|
|
34
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // supabase/functions/set-custom-claims/index.ts
|
|
39
|
+
* import { createSetCustomClaims } from '@donotdev/functions/supabase';
|
|
40
|
+
* Deno.serve(createSetCustomClaims());
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @version 0.0.1
|
|
44
|
+
* @since 0.6.0
|
|
45
|
+
*/
|
|
46
|
+
export function createSetCustomClaims() {
|
|
47
|
+
return createSupabaseHandler(
|
|
48
|
+
'set-custom-claims',
|
|
49
|
+
setCustomClaimsSchema,
|
|
50
|
+
async (data, ctx) => {
|
|
51
|
+
// Get current app_metadata
|
|
52
|
+
const { data: { user }, error: getUserError } =
|
|
53
|
+
await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
|
|
54
|
+
if (getUserError || !user) {
|
|
55
|
+
throw new Error('User not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Merge new claims with existing app_metadata
|
|
59
|
+
const existingClaims = (user.app_metadata ?? {}) as Record<string, unknown>;
|
|
60
|
+
const updatedClaims = { ...existingClaims, ...data.customClaims };
|
|
61
|
+
|
|
62
|
+
// Update app_metadata
|
|
63
|
+
const { error: updateError } =
|
|
64
|
+
await ctx.supabaseAdmin.auth.admin.updateUserById(ctx.uid, {
|
|
65
|
+
app_metadata: updatedClaims,
|
|
66
|
+
});
|
|
67
|
+
if (updateError) throw updateError;
|
|
68
|
+
|
|
69
|
+
return { success: true, customClaims: updatedClaims };
|
|
70
|
+
},
|
|
71
|
+
'user',
|
|
72
|
+
);
|
|
73
|
+
}
|