@donotdev/functions 0.0.10 → 0.0.12
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 +1 -1
- package/package.json +32 -8
- 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/createCheckoutSession.ts +3 -1
- 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/crud/aggregate.ts +20 -5
- package/src/firebase/crud/create.ts +31 -7
- package/src/firebase/crud/list.ts +36 -24
- 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 +2 -2
- 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 +22 -60
- package/src/shared/firebase.ts +1 -25
- package/src/shared/index.ts +2 -1
- package/src/shared/logger.ts +3 -7
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
- package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
- package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
- package/src/shared/utils/external/subscription.ts +12 -2
- 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 +170 -66
- package/src/supabase/auth/deleteAccount.ts +52 -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 +306 -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 +388 -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';
|
|
@@ -19,6 +20,11 @@ import {
|
|
|
19
20
|
initFirebaseAdmin,
|
|
20
21
|
} from '@donotdev/firebase/server';
|
|
21
22
|
|
|
23
|
+
import {
|
|
24
|
+
assertAuthenticated as internalAssertAuthenticated,
|
|
25
|
+
assertAdmin as internalAssertAdmin,
|
|
26
|
+
} from './utils/internal/auth.js';
|
|
27
|
+
|
|
22
28
|
// Re-export DoNotDevError for external use
|
|
23
29
|
export { DoNotDevError };
|
|
24
30
|
|
|
@@ -101,17 +107,25 @@ export const stripe = new Proxy({} as Stripe, {
|
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
|
-
* Assert that a user is authenticated
|
|
110
|
+
* Assert that a user is authenticated from a Firebase callable auth context.
|
|
105
111
|
*
|
|
106
|
-
* @
|
|
112
|
+
* @deprecated Use `assertAuthenticated` from `shared/utils/internal/auth.js` instead.
|
|
113
|
+
* This wrapper extracts uid from a Firebase callable auth context and delegates
|
|
114
|
+
* to the canonical version.
|
|
115
|
+
*
|
|
116
|
+
* @param auth - Firebase callable request auth context (object with `.uid`)
|
|
117
|
+
* @returns The authenticated user's uid
|
|
118
|
+
*
|
|
119
|
+
* @version 0.0.3
|
|
107
120
|
* @since 0.0.1
|
|
108
121
|
* @author AMBROISE PARK Consulting
|
|
109
122
|
*/
|
|
110
123
|
export function assertAuthenticated(auth: any): string {
|
|
111
|
-
|
|
124
|
+
const uid = auth?.uid;
|
|
125
|
+
if (!uid) {
|
|
112
126
|
throw new Error('User must be authenticated');
|
|
113
127
|
}
|
|
114
|
-
return
|
|
128
|
+
return internalAssertAuthenticated(uid);
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
/**
|
|
@@ -220,68 +234,91 @@ export function createErrorResponse(error: unknown): {
|
|
|
220
234
|
}
|
|
221
235
|
|
|
222
236
|
/**
|
|
223
|
-
* Updates user subscription in Firebase
|
|
224
|
-
* TODO: Implement this function based on your Firebase setup
|
|
237
|
+
* Updates user subscription in Firebase.
|
|
225
238
|
*
|
|
226
|
-
*
|
|
239
|
+
* **Architecture decision — throwing stubs for billing functions:**
|
|
240
|
+
* These subscription functions (`updateUserSubscription`, `getUserSubscription`,
|
|
241
|
+
* `cancelUserSubscription`, `handleSubscriptionCancellation`) are intentional
|
|
242
|
+
* placeholder implementations. They exist so the framework compiles and
|
|
243
|
+
* type-checks out of the box, but throw at runtime to surface missing
|
|
244
|
+
* integration early. Consumer apps replace them with their billing provider
|
|
245
|
+
* integration (Stripe, Paddle, LemonSqueezy, etc.) via the documented
|
|
246
|
+
* `shared/billing/helpers/` modules.
|
|
247
|
+
*
|
|
248
|
+
* C10: This was a silent no-op stub. It now throws to surface the missing
|
|
249
|
+
* implementation at startup rather than silently dropping webhook events.
|
|
250
|
+
* Use `updateUserSubscription` from `shared/billing/helpers/updateUserSubscription.ts`
|
|
251
|
+
* (which requires an authProvider) instead.
|
|
252
|
+
*
|
|
253
|
+
* @deprecated Use shared/billing/helpers/updateUserSubscription.ts
|
|
254
|
+
* @version 0.0.2
|
|
227
255
|
* @since 0.0.1
|
|
228
256
|
* @author AMBROISE PARK Consulting
|
|
229
257
|
*/
|
|
230
258
|
export async function updateUserSubscription(
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
_firebaseUid: string,
|
|
260
|
+
_subscription: any
|
|
233
261
|
): Promise<void> {
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
throw new DoNotDevError(
|
|
263
|
+
'updateUserSubscription stub called — import from shared/billing/helpers/updateUserSubscription.ts and supply an authProvider',
|
|
264
|
+
'unimplemented'
|
|
265
|
+
);
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
/**
|
|
239
|
-
* Gets user subscription from Firebase
|
|
240
|
-
* TODO: Implement this function based on your Firebase setup
|
|
269
|
+
* Gets user subscription from Firebase.
|
|
241
270
|
*
|
|
242
|
-
*
|
|
271
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
272
|
+
*
|
|
273
|
+
* @deprecated Implement directly using Firebase Admin SDK or Stripe API.
|
|
274
|
+
* @version 0.0.2
|
|
243
275
|
* @since 0.0.1
|
|
244
276
|
* @author AMBROISE PARK Consulting
|
|
245
277
|
*/
|
|
246
|
-
export async function getUserSubscription(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
export async function getUserSubscription(_firebaseUid: string): Promise<any> {
|
|
279
|
+
throw new DoNotDevError(
|
|
280
|
+
'getUserSubscription stub called — implement using Firebase Admin SDK or Stripe API',
|
|
281
|
+
'unimplemented'
|
|
282
|
+
);
|
|
250
283
|
}
|
|
251
284
|
|
|
252
285
|
/**
|
|
253
|
-
* Cancels user subscription
|
|
254
|
-
* TODO: Implement this function based on your Firebase setup
|
|
286
|
+
* Cancels user subscription.
|
|
255
287
|
*
|
|
256
|
-
*
|
|
288
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
289
|
+
*
|
|
290
|
+
* @deprecated Use cancelUserSubscription from shared/billing/helpers/subscriptionManagement.ts
|
|
291
|
+
* @version 0.0.2
|
|
257
292
|
* @since 0.0.1
|
|
258
293
|
* @author AMBROISE PARK Consulting
|
|
259
294
|
*/
|
|
260
295
|
export async function cancelUserSubscription(
|
|
261
|
-
|
|
262
|
-
|
|
296
|
+
_firebaseUid: string,
|
|
297
|
+
_subscription: any
|
|
263
298
|
): Promise<void> {
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
throw new DoNotDevError(
|
|
300
|
+
'cancelUserSubscription stub called — import from shared/billing/helpers/subscriptionManagement.ts',
|
|
301
|
+
'unimplemented'
|
|
302
|
+
);
|
|
266
303
|
}
|
|
267
304
|
|
|
268
305
|
/**
|
|
269
|
-
* Handles subscription cancellation
|
|
270
|
-
* TODO: Implement this function based on your Firebase setup
|
|
306
|
+
* Handles subscription cancellation.
|
|
271
307
|
*
|
|
272
|
-
*
|
|
308
|
+
* C10: This was a silent no-op stub. Now throws to surface the missing implementation.
|
|
309
|
+
*
|
|
310
|
+
* @deprecated Implement using Stripe webhook events and updateUserSubscription.
|
|
311
|
+
* @version 0.0.2
|
|
273
312
|
* @since 0.0.1
|
|
274
313
|
* @author AMBROISE PARK Consulting
|
|
275
314
|
*/
|
|
276
315
|
export async function handleSubscriptionCancellation(
|
|
277
|
-
|
|
278
|
-
|
|
316
|
+
_firebaseUid: string,
|
|
317
|
+
_subscription: any
|
|
279
318
|
): Promise<void> {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'
|
|
283
|
-
firebaseUid,
|
|
284
|
-
subscription.id
|
|
319
|
+
throw new DoNotDevError(
|
|
320
|
+
'handleSubscriptionCancellation stub called — handle via Stripe webhook events',
|
|
321
|
+
'unimplemented'
|
|
285
322
|
);
|
|
286
323
|
}
|
|
287
324
|
|
|
@@ -289,35 +326,15 @@ export async function handleSubscriptionCancellation(
|
|
|
289
326
|
* Asserts that a user has admin privileges
|
|
290
327
|
* Uses role hierarchy: super > admin > user > guest
|
|
291
328
|
*
|
|
292
|
-
* @
|
|
329
|
+
* @deprecated Use `assertAdmin` from `shared/utils/internal/auth.js` instead.
|
|
330
|
+
* This is a thin wrapper that delegates to the canonical provider-agnostic version.
|
|
331
|
+
*
|
|
332
|
+
* @version 0.0.3
|
|
293
333
|
* @since 0.0.1
|
|
294
334
|
* @author AMBROISE PARK Consulting
|
|
295
335
|
*/
|
|
296
336
|
export async function assertAdmin(uid: string): Promise<string> {
|
|
297
|
-
|
|
298
|
-
throw new DoNotDevError('Authentication required', 'unauthenticated');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
const user = await getFirebaseAdminAuth().getUser(uid);
|
|
303
|
-
const claims = user.customClaims || {};
|
|
304
|
-
|
|
305
|
-
// Check role claim first (standard pattern)
|
|
306
|
-
const role = claims.role;
|
|
307
|
-
if (role === 'admin' || role === 'super') {
|
|
308
|
-
return uid;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Fallback: check legacy boolean flags
|
|
312
|
-
if (claims.isAdmin === true || claims.isSuper === true) {
|
|
313
|
-
return uid;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
throw new DoNotDevError('Admin privileges required', 'permission-denied');
|
|
317
|
-
} catch (error) {
|
|
318
|
-
if (error instanceof DoNotDevError) throw error;
|
|
319
|
-
throw new DoNotDevError('Failed to verify admin status', 'internal');
|
|
320
|
-
}
|
|
337
|
+
return internalAssertAdmin(uid);
|
|
321
338
|
}
|
|
322
339
|
|
|
323
340
|
/**
|
|
@@ -335,6 +352,54 @@ export function validateDocument(data: any, schema?: any): void {
|
|
|
335
352
|
if (Array.isArray(data)) {
|
|
336
353
|
throw new Error('Document data cannot be an array');
|
|
337
354
|
}
|
|
355
|
+
|
|
356
|
+
// Run Valibot schema validation when a schema is provided
|
|
357
|
+
if (schema) {
|
|
358
|
+
try {
|
|
359
|
+
v.parse(schema, data);
|
|
360
|
+
} catch (error: any) {
|
|
361
|
+
if (error?.issues) {
|
|
362
|
+
const messages = error.issues
|
|
363
|
+
.map(
|
|
364
|
+
(issue: v.BaseIssue<unknown>) =>
|
|
365
|
+
`${issue.path?.map((p: any) => p.key).join('.') || 'root'}: ${issue.message}`
|
|
366
|
+
)
|
|
367
|
+
.join('; ');
|
|
368
|
+
throw new DoNotDevError(
|
|
369
|
+
`Validation failed: ${messages}`,
|
|
370
|
+
'invalid-argument',
|
|
371
|
+
{ details: { validationErrors: error.issues } }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Validates a Firestore collection name from client-supplied schema.
|
|
381
|
+
*
|
|
382
|
+
* W22: Vercel CRUD handlers accept `schema` (including collection name) from
|
|
383
|
+
* the client. This is a known design limitation. As a defense-in-depth measure,
|
|
384
|
+
* reject collection names that could be used for path traversal or access to
|
|
385
|
+
* internal collections.
|
|
386
|
+
*
|
|
387
|
+
* @param name - Collection name to validate
|
|
388
|
+
* @throws Error if the name is unsafe
|
|
389
|
+
*
|
|
390
|
+
* @version 0.0.1
|
|
391
|
+
* @since 0.0.1
|
|
392
|
+
* @author AMBROISE PARK Consulting
|
|
393
|
+
*/
|
|
394
|
+
export function validateCollectionName(name: string): void {
|
|
395
|
+
if (!name || typeof name !== 'string') {
|
|
396
|
+
throw new Error('Collection name is required');
|
|
397
|
+
}
|
|
398
|
+
if (name.includes('/') || name.includes('..') || name.startsWith('_')) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
'Invalid collection name: must not contain "/", "..", or start with "_"'
|
|
401
|
+
);
|
|
402
|
+
}
|
|
338
403
|
}
|
|
339
404
|
|
|
340
405
|
/**
|
|
@@ -345,9 +410,16 @@ export function validateDocument(data: any, schema?: any): void {
|
|
|
345
410
|
* @author AMBROISE PARK Consulting
|
|
346
411
|
*/
|
|
347
412
|
export async function verifyFirebaseAuthToken(token: string): Promise<string> {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
413
|
+
if (!token) {
|
|
414
|
+
throw new DoNotDevError('Missing authentication token', 'unauthenticated');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const decodedToken = await getFirebaseAdminAuth().verifyIdToken(token);
|
|
419
|
+
return decodedToken.uid;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new DoNotDevError('Invalid or expired token', 'unauthenticated');
|
|
422
|
+
}
|
|
351
423
|
}
|
|
352
424
|
|
|
353
425
|
/**
|
|
@@ -357,7 +429,39 @@ export async function verifyFirebaseAuthToken(token: string): Promise<string> {
|
|
|
357
429
|
* @since 0.0.1
|
|
358
430
|
* @author AMBROISE PARK Consulting
|
|
359
431
|
*/
|
|
360
|
-
export function findReferences(
|
|
361
|
-
|
|
362
|
-
|
|
432
|
+
export async function findReferences(
|
|
433
|
+
collection: string,
|
|
434
|
+
docId: string,
|
|
435
|
+
referenceMetadata?: {
|
|
436
|
+
incoming?: Array<{
|
|
437
|
+
sourceCollection: string;
|
|
438
|
+
sourceField: string;
|
|
439
|
+
}>;
|
|
440
|
+
}
|
|
441
|
+
): Promise<Array<{ collection: string; field: string; count: number }>> {
|
|
442
|
+
const references: Array<{ collection: string; field: string; count: number }> = [];
|
|
443
|
+
|
|
444
|
+
if (!referenceMetadata?.incoming?.length) {
|
|
445
|
+
return references;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const db = getFirebaseAdminFirestore();
|
|
449
|
+
|
|
450
|
+
for (const ref of referenceMetadata.incoming) {
|
|
451
|
+
const snapshot = await db
|
|
452
|
+
.collection(ref.sourceCollection)
|
|
453
|
+
.where(ref.sourceField, '==', docId)
|
|
454
|
+
.limit(1)
|
|
455
|
+
.get();
|
|
456
|
+
|
|
457
|
+
if (!snapshot.empty) {
|
|
458
|
+
references.push({
|
|
459
|
+
collection: ref.sourceCollection,
|
|
460
|
+
field: ref.sourceField,
|
|
461
|
+
count: snapshot.size,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return references;
|
|
363
467
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Handler
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a Supabase Edge Function handler for account deletion.
|
|
28
|
+
*
|
|
29
|
+
* @returns `(req: Request) => Promise<Response>` handler
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // supabase/functions/delete-account/index.ts
|
|
34
|
+
* import { createDeleteAccount } from '@donotdev/functions/supabase';
|
|
35
|
+
* Deno.serve(createDeleteAccount());
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @version 0.0.1
|
|
39
|
+
* @since 0.5.0
|
|
40
|
+
*/
|
|
41
|
+
export function createDeleteAccount() {
|
|
42
|
+
return createSupabaseHandler(
|
|
43
|
+
'delete-account',
|
|
44
|
+
deleteAccountSchema,
|
|
45
|
+
async (_data, ctx) => {
|
|
46
|
+
const { error } = await ctx.supabaseAdmin.auth.admin.deleteUser(ctx.uid);
|
|
47
|
+
if (error) throw error;
|
|
48
|
+
|
|
49
|
+
return { success: true };
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -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
|
+
}
|