mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 16:51:13 -05:00
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:
@@ -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;
|
||||
// Get user from Supabase
|
||||
const { data: { user: authUser }, error: userError } = await supabase.auth.getUser();
|
||||
|
||||
addSpanEvent(span, 'authentication_success', { userId });
|
||||
span.attributes['user.id'] = userId;
|
||||
} catch (error) {
|
||||
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);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { addSpanEvent } from '../_shared/logger.ts';
|
||||
|
||||
interface InboundEmailPayload {
|
||||
from: string;
|
||||
@@ -17,27 +15,15 @@ interface InboundEmailPayload {
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
const handler = async (req: Request): Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
const tracking = startRequest();
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
);
|
||||
|
||||
const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext): Promise<Response> => {
|
||||
const payload: InboundEmailPayload = await req.json();
|
||||
const { from, to, subject, text, html, messageId, inReplyTo, references, headers } = payload;
|
||||
|
||||
edgeLogger.info('Inbound email received', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'email_received', {
|
||||
from,
|
||||
to,
|
||||
messageId
|
||||
messageId,
|
||||
hasInReplyTo: !!inReplyTo
|
||||
});
|
||||
|
||||
// Extract thread ID from headers or inReplyTo
|
||||
@@ -48,17 +34,11 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
const isNewEmail = !threadId;
|
||||
|
||||
if (isNewEmail) {
|
||||
edgeLogger.info('New direct email received (no thread ID)', {
|
||||
requestId: tracking.requestId,
|
||||
from,
|
||||
subject,
|
||||
messageId
|
||||
});
|
||||
addSpanEvent(span, 'new_direct_email', { from, subject });
|
||||
}
|
||||
|
||||
// Find or create submission
|
||||
let submission = null;
|
||||
let submissionError = null;
|
||||
|
||||
if (isNewEmail) {
|
||||
// Extract sender email
|
||||
@@ -80,8 +60,7 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
submission = existingRecent;
|
||||
threadId = existingRecent.thread_id;
|
||||
|
||||
edgeLogger.info('Using existing recent submission', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'duplicate_submission_found', {
|
||||
submissionId: existingRecent.id,
|
||||
ticketNumber: existingRecent.ticket_number
|
||||
});
|
||||
@@ -103,11 +82,8 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
.single();
|
||||
|
||||
if (createError || !newSubmission) {
|
||||
edgeLogger.error('Failed to create submission from direct email', {
|
||||
requestId: tracking.requestId,
|
||||
error: createError
|
||||
});
|
||||
return createErrorResponse(createError, 500, corsHeaders);
|
||||
addSpanEvent(span, 'submission_creation_failed', { error: createError });
|
||||
throw createError;
|
||||
}
|
||||
|
||||
submission = newSubmission;
|
||||
@@ -119,26 +95,21 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
.update({ thread_id: threadId })
|
||||
.eq('id', newSubmission.id);
|
||||
|
||||
edgeLogger.info('Created new submission from direct email', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'submission_created', {
|
||||
submissionId: newSubmission.id,
|
||||
ticketNumber: newSubmission.ticket_number,
|
||||
threadId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// EXISTING LOGIC: Find submission by thread_id or ticket_number
|
||||
// Find submission by thread_id or ticket_number
|
||||
const ticketMatch = threadId.match(/(?:ticket-)?(TW-\d+)/i);
|
||||
const ticketNumber = ticketMatch ? ticketMatch[1] : null;
|
||||
|
||||
edgeLogger.info('Thread ID extracted', {
|
||||
requestId: tracking.requestId,
|
||||
rawThreadId: threadId,
|
||||
ticketNumber
|
||||
});
|
||||
addSpanEvent(span, 'thread_lookup', { threadId, ticketNumber });
|
||||
|
||||
// Strategy 1: Try exact thread_id match
|
||||
const { data: submissionByThreadId, error: error1 } = await supabase
|
||||
const { data: submissionByThreadId } = await supabase
|
||||
.from('contact_submissions')
|
||||
.select('id, email, status, ticket_number')
|
||||
.eq('thread_id', threadId)
|
||||
@@ -146,6 +117,7 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
|
||||
if (submissionByThreadId) {
|
||||
submission = submissionByThreadId;
|
||||
addSpanEvent(span, 'submission_found_by_thread_id', { submissionId: submission.id });
|
||||
} else if (ticketNumber) {
|
||||
// Strategy 2: Try ticket_number match
|
||||
const { data: submissionByTicket, error: error2 } = await supabase
|
||||
@@ -164,44 +136,39 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
.update({ thread_id: threadId })
|
||||
.eq('id', submissionByTicket.id);
|
||||
|
||||
edgeLogger.info('Updated submission thread_id', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'thread_id_updated', {
|
||||
submissionId: submissionByTicket.id,
|
||||
oldThreadId: submissionByTicket.thread_id,
|
||||
newThreadId: threadId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
submissionError = error2;
|
||||
}
|
||||
} else {
|
||||
submissionError = error1;
|
||||
}
|
||||
|
||||
if (submissionError || !submission) {
|
||||
edgeLogger.warn('Submission not found for thread ID', {
|
||||
requestId: tracking.requestId,
|
||||
threadId,
|
||||
ticketNumber,
|
||||
error: submissionError
|
||||
});
|
||||
addSpanEvent(span, 'submission_found_by_ticket_number', { submissionId: submission.id });
|
||||
} else {
|
||||
addSpanEvent(span, 'submission_not_found', { threadId, ticketNumber });
|
||||
return new Response(JSON.stringify({ success: false, reason: 'submission_not_found' }), {
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addSpanEvent(span, 'submission_not_found', { threadId });
|
||||
return new Response(JSON.stringify({ success: false, reason: 'submission_not_found' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Verify sender email matches (only for existing submissions)
|
||||
const senderEmail = from.match(/<(.+)>/)?.[1] || from;
|
||||
if (senderEmail.toLowerCase() !== submission.email.toLowerCase()) {
|
||||
edgeLogger.warn('Sender email mismatch', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'email_mismatch', {
|
||||
expected: submission.email,
|
||||
received: senderEmail
|
||||
});
|
||||
return new Response(JSON.stringify({ success: false, reason: 'email_mismatch' }), {
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -229,39 +196,35 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
edgeLogger.error('Failed to insert inbound email thread', {
|
||||
requestId: tracking.requestId,
|
||||
error: insertError
|
||||
});
|
||||
return createErrorResponse(insertError, 500, corsHeaders);
|
||||
addSpanEvent(span, 'thread_insert_failed', { error: insertError });
|
||||
throw insertError;
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'thread_inserted');
|
||||
|
||||
// Update submission status if pending
|
||||
if (submission.status === 'pending') {
|
||||
await supabase
|
||||
.from('contact_submissions')
|
||||
.update({ status: 'in_progress' })
|
||||
.eq('id', submission.id);
|
||||
|
||||
addSpanEvent(span, 'status_updated', { newStatus: 'in_progress' });
|
||||
}
|
||||
|
||||
edgeLogger.info('Inbound email processed successfully', {
|
||||
requestId: tracking.requestId,
|
||||
submissionId: submission.id,
|
||||
duration: endRequest(tracking)
|
||||
});
|
||||
addSpanEvent(span, 'email_processed', { submissionId: submission.id });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
edgeLogger.error('Unexpected error in receive-inbound-email', {
|
||||
requestId: tracking.requestId,
|
||||
error: formatEdgeError(error)
|
||||
});
|
||||
return createErrorResponse(error, 500, corsHeaders);
|
||||
}
|
||||
};
|
||||
|
||||
serve(handler);
|
||||
serve(createEdgeFunction({
|
||||
name: 'receive-inbound-email',
|
||||
requireAuth: false,
|
||||
useServiceRole: true,
|
||||
corsHeaders,
|
||||
logRequests: true,
|
||||
logResponses: true,
|
||||
}, handler));
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { addSpanEvent } from '../_shared/logger.ts';
|
||||
|
||||
interface AdminReplyRequest {
|
||||
submissionId: string;
|
||||
@@ -11,59 +9,29 @@ interface AdminReplyRequest {
|
||||
replySubject?: string;
|
||||
}
|
||||
|
||||
const handler = async (req: Request): Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
const tracking = startRequest();
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return createErrorResponse({ message: 'Unauthorized' }, 401, corsHeaders);
|
||||
}
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_ANON_KEY')!,
|
||||
{ global: { headers: { Authorization: authHeader } } }
|
||||
);
|
||||
|
||||
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||
if (userError || !user) {
|
||||
return createErrorResponse({ message: 'Unauthorized' }, 401, corsHeaders);
|
||||
}
|
||||
|
||||
// Verify admin, moderator, or superuser role
|
||||
const { data: isSuperuser } = await supabase
|
||||
.rpc('has_role', { _user_id: user.id, _role: 'superuser' });
|
||||
const { data: isAdmin } = await supabase
|
||||
.rpc('has_role', { _user_id: user.id, _role: 'admin' });
|
||||
const { data: isModerator } = await supabase
|
||||
.rpc('has_role', { _user_id: user.id, _role: 'moderator' });
|
||||
|
||||
if (!isSuperuser && !isAdmin && !isModerator) {
|
||||
edgeLogger.warn('Non-privileged user attempted email reply', {
|
||||
requestId: tracking.requestId,
|
||||
userId: user.id
|
||||
});
|
||||
return createErrorResponse({ message: 'Admin access required' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
const handler = async (req: Request, { supabase, user, span, requestId }: EdgeFunctionContext): Promise<Response> => {
|
||||
const body: AdminReplyRequest = await req.json();
|
||||
const { submissionId, replyBody, replySubject } = body;
|
||||
|
||||
// Validate request
|
||||
if (!submissionId || !replyBody) {
|
||||
return createErrorResponse({ message: 'Missing required fields' }, 400, corsHeaders);
|
||||
addSpanEvent(span, 'validation_failed', { reason: 'missing_fields' });
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Missing required fields', requestId }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (replyBody.length < 10 || replyBody.length > 5000) {
|
||||
return createErrorResponse({
|
||||
message: 'Reply must be between 10 and 5000 characters'
|
||||
}, 400, corsHeaders);
|
||||
addSpanEvent(span, 'validation_failed', { reason: 'invalid_length', length: replyBody.length });
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Reply must be between 10 and 5000 characters', requestId }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'request_validated', { submissionId });
|
||||
|
||||
// Get admin email from environment variable
|
||||
const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS') || 'admins@thrillwiki.com';
|
||||
const adminDisplayName = Deno.env.get('ADMIN_EMAIL_DISPLAY_NAME') || 'ThrillWiki Admin';
|
||||
@@ -85,9 +53,18 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
.single();
|
||||
|
||||
if (fetchError || !submission) {
|
||||
return createErrorResponse({ message: 'Submission not found' }, 404, corsHeaders);
|
||||
addSpanEvent(span, 'submission_not_found', { submissionId });
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Submission not found', requestId }),
|
||||
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'submission_fetched', {
|
||||
ticketNumber: submission.ticket_number,
|
||||
recipientEmail: submission.email
|
||||
});
|
||||
|
||||
// Fetch email signature from admin settings
|
||||
const { data: signatureSetting } = await supabase
|
||||
.from('admin_settings')
|
||||
@@ -112,9 +89,11 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
.gte('created_at', oneHourAgo);
|
||||
|
||||
if (count && count >= 10) {
|
||||
return createErrorResponse({
|
||||
message: 'Rate limit exceeded. Max 10 replies per hour.'
|
||||
}, 429, corsHeaders);
|
||||
addSpanEvent(span, 'rate_limit_exceeded', { count });
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Rate limit exceeded. Max 10 replies per hour.', requestId }),
|
||||
{ status: 429, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const ticketNumber = submission.ticket_number || 'UNKNOWN';
|
||||
@@ -137,6 +116,12 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
? [originalMessageId, previousMessages[0].message_id].join(' ')
|
||||
: originalMessageId;
|
||||
|
||||
addSpanEvent(span, 'sending_email', {
|
||||
messageId,
|
||||
recipient: submission.email,
|
||||
subject: finalSubject
|
||||
});
|
||||
|
||||
// Send email via ForwardEmail
|
||||
const forwardEmailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
||||
method: 'POST',
|
||||
@@ -161,14 +146,15 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
|
||||
if (!forwardEmailResponse.ok) {
|
||||
const errorText = await forwardEmailResponse.text();
|
||||
edgeLogger.error('ForwardEmail API error', {
|
||||
requestId: tracking.requestId,
|
||||
addSpanEvent(span, 'email_send_failed', {
|
||||
status: forwardEmailResponse.status,
|
||||
error: errorText
|
||||
});
|
||||
return createErrorResponse({ message: 'Failed to send email' }, 500, corsHeaders);
|
||||
throw new Error(`ForwardEmail API error: ${errorText}`);
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'email_sent', { messageId });
|
||||
|
||||
// Insert email thread record
|
||||
const { error: insertError } = await supabase
|
||||
.from('contact_email_threads')
|
||||
@@ -190,10 +176,9 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
edgeLogger.error('Failed to insert email thread', {
|
||||
requestId: tracking.requestId,
|
||||
error: insertError
|
||||
});
|
||||
addSpanEvent(span, 'thread_insert_failed', { error: insertError });
|
||||
} else {
|
||||
addSpanEvent(span, 'thread_inserted');
|
||||
}
|
||||
|
||||
// Update submission
|
||||
@@ -206,6 +191,8 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
})
|
||||
.eq('id', submissionId);
|
||||
|
||||
addSpanEvent(span, 'submission_updated');
|
||||
|
||||
// Audit log
|
||||
await supabase
|
||||
.from('admin_audit_log')
|
||||
@@ -220,24 +207,19 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
}
|
||||
});
|
||||
|
||||
edgeLogger.info('Admin email reply sent successfully', {
|
||||
requestId: tracking.requestId,
|
||||
submissionId,
|
||||
duration: endRequest(tracking)
|
||||
});
|
||||
addSpanEvent(span, 'audit_logged');
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, messageId }),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
edgeLogger.error('Unexpected error in send-admin-email-reply', {
|
||||
requestId: tracking.requestId,
|
||||
error: formatEdgeError(error)
|
||||
});
|
||||
return createErrorResponse(error, 500, corsHeaders);
|
||||
}
|
||||
};
|
||||
|
||||
serve(handler);
|
||||
serve(createEdgeFunction({
|
||||
name: 'send-admin-email-reply',
|
||||
requireAuth: true,
|
||||
requiredRoles: ['superuser', 'admin', 'moderator'],
|
||||
corsHeaders,
|
||||
logRequests: true,
|
||||
logResponses: true,
|
||||
}, handler));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { startRequest, endRequest, edgeLogger } from "../_shared/logger.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { addSpanEvent } from '../_shared/logger.ts';
|
||||
|
||||
// Comprehensive list of disposable email domains
|
||||
const DISPOSABLE_DOMAINS = new Set([
|
||||
@@ -64,35 +64,24 @@ interface ValidationResult {
|
||||
valid: boolean;
|
||||
reason?: string;
|
||||
suggestions?: string[];
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
const handler = async (req: Request): Promise<Response> => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
const tracking = startRequest('validate-email');
|
||||
|
||||
try {
|
||||
const handler = async (req: Request, { span, requestId }: EdgeFunctionContext): Promise<Response> => {
|
||||
const { email }: ValidateEmailRequest = await req.json();
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
endRequest(tracking, 400, 'Email address is required');
|
||||
addSpanEvent(span, 'validation_failed', { reason: 'missing_email' });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Email address is required',
|
||||
requestId: tracking.requestId
|
||||
requestId
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -100,36 +89,28 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
// Basic email format validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
endRequest(tracking, 400, 'Invalid email format');
|
||||
addSpanEvent(span, 'validation_failed', { reason: 'invalid_format' });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Invalid email format',
|
||||
requestId: tracking.requestId
|
||||
requestId
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Extract domain
|
||||
const domain = email.split('@')[1].toLowerCase();
|
||||
addSpanEvent(span, 'domain_extracted', { domain });
|
||||
|
||||
// Check if domain is disposable
|
||||
if (DISPOSABLE_DOMAINS.has(domain)) {
|
||||
edgeLogger.info('Blocked disposable email domain', {
|
||||
domain,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
endRequest(tracking, 400, 'Disposable email domain blocked');
|
||||
addSpanEvent(span, 'disposable_domain_blocked', { domain });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -140,67 +121,34 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
'Use your work or school email address',
|
||||
'Use an email from your own domain'
|
||||
],
|
||||
requestId: tracking.requestId
|
||||
requestId
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Email is valid
|
||||
edgeLogger.info('Email validated successfully', {
|
||||
email,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
endRequest(tracking, 200);
|
||||
addSpanEvent(span, 'email_validated', { email });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: true,
|
||||
requestId: tracking.requestId
|
||||
requestId
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = formatEdgeError(error);
|
||||
edgeLogger.error('Error in validate-email function', {
|
||||
error: errorMessage,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
endRequest(tracking, 500, error.message);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Internal server error during email validation',
|
||||
requestId: tracking.requestId
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
serve(handler);
|
||||
serve(createEdgeFunction({
|
||||
name: 'validate-email',
|
||||
requireAuth: false,
|
||||
corsHeaders,
|
||||
logRequests: true,
|
||||
logResponses: true,
|
||||
}, handler));
|
||||
|
||||
Reference in New Issue
Block a user