Migrate 3 edge functions to wrapper

- Refactor validate-email, receive-inbound-email, and send-admin-email-reply to use createEdgeFunction wrapper with automatic error handling, tracing, and reduced boilerplate.
- enrich wrapper to support service-role usage and role-based authorization context for complex flows.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 20:09:42 +00:00
parent afe7a93f69
commit 4040fd783e
4 changed files with 563 additions and 596 deletions

View File

@@ -21,10 +21,13 @@ import {
} from './logger.ts';
import { formatEdgeError, toError } from './errorFormatter.ts';
import { ValidationError, logValidationError } from './typeValidation.ts';
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
export interface EdgeFunctionConfig {
name: string;
requireAuth?: boolean;
requiredRoles?: string[];
useServiceRole?: boolean;
corsHeaders?: HeadersInit;
logRequests?: boolean;
logResponses?: boolean;
@@ -34,6 +37,8 @@ export interface EdgeFunctionContext {
requestId: string;
span: Span;
userId?: string;
user?: any;
supabase: any;
}
export type EdgeFunctionHandler = (
@@ -51,6 +56,8 @@ export function wrapEdgeFunction(
const {
name,
requireAuth = true,
requiredRoles = [],
useServiceRole = false,
corsHeaders = {},
logRequests = true,
logResponses = true,
@@ -100,13 +107,39 @@ export function wrapEdgeFunction(
try {
// ====================================================================
// STEP 4: Authentication (if required)
// STEP 4: Create Supabase client
// ====================================================================
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const authHeader = req.headers.get('Authorization');
let supabase;
if (useServiceRole) {
// Use service role key for backend operations
const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
supabase = createClient(supabaseUrl, serviceRoleKey);
addSpanEvent(span, 'supabase_client_created', { type: 'service_role' });
} else if (authHeader) {
// Use anon key with user's auth header
const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
supabase = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } }
});
addSpanEvent(span, 'supabase_client_created', { type: 'authenticated' });
} else {
// Use anon key without auth
const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
supabase = createClient(supabaseUrl, anonKey);
addSpanEvent(span, 'supabase_client_created', { type: 'anonymous' });
}
// ====================================================================
// STEP 5: Authentication (if required)
// ====================================================================
let userId: string | undefined;
let user: any = undefined;
if (requireAuth) {
addSpanEvent(span, 'authentication_start');
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
addSpanEvent(span, 'authentication_failed', { reason: 'missing_header' });
@@ -125,21 +158,15 @@ export function wrapEdgeFunction(
);
}
// Extract user ID from JWT (simplified - extend as needed)
try {
// Note: In production, validate the JWT properly
const token = authHeader.replace('Bearer ', '');
const payload = JSON.parse(atob(token.split('.')[1]));
userId = payload.sub;
addSpanEvent(span, 'authentication_success', { userId });
span.attributes['user.id'] = userId;
} catch (error) {
// Get user from Supabase
const { data: { user: authUser }, error: userError } = await supabase.auth.getUser();
if (userError || !authUser) {
addSpanEvent(span, 'authentication_failed', {
reason: 'invalid_token',
error: formatEdgeError(error)
error: formatEdgeError(userError)
});
endSpan(span, 'error', error);
endSpan(span, 'error', userError);
logSpan(span);
return new Response(
@@ -153,10 +180,55 @@ export function wrapEdgeFunction(
}
);
}
user = authUser;
userId = authUser.id;
addSpanEvent(span, 'authentication_success', { userId });
span.attributes['user.id'] = userId;
// ====================================================================
// STEP 6: Role verification (if required)
// ====================================================================
if (requiredRoles.length > 0) {
addSpanEvent(span, 'role_check_start', { requiredRoles });
let hasRequiredRole = false;
for (const role of requiredRoles) {
const { data: hasRole } = await supabase
.rpc('has_role', { _user_id: userId, _role: role });
if (hasRole) {
hasRequiredRole = true;
addSpanEvent(span, 'role_check_success', { role });
break;
}
}
if (!hasRequiredRole) {
addSpanEvent(span, 'role_check_failed', {
userId,
requiredRoles
});
endSpan(span, 'error');
logSpan(span);
return new Response(
JSON.stringify({
error: 'Insufficient permissions',
requestId
}),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
}
}
// ====================================================================
// STEP 5: Execute handler
// STEP 7: Execute handler
// ====================================================================
addSpanEvent(span, 'handler_start');
@@ -164,6 +236,8 @@ export function wrapEdgeFunction(
requestId,
span,
userId,
user,
supabase,
};
const response = await handler(req, context);