mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 04:31:13 -05:00
Migrate Phase 2 admin edges
Migrate five admin/moderator edge functions (merge-contact-tickets, send-escalation-notification, notify-moderators-report, notify-moderators-submission, send-password-added-email) to use createEdgeFunction wrapper. Remove manual CORS, auth, service-client setup, logging, and error handling. Implement handler with EdgeFunctionContext, apply appropriate wrapper config (requireAuth, requiredRoles/useServiceRole, corsEnabled, enableTracing, rateLimitTier). Replace edgeLogger with span events, maintain core business logic and retry/email integration patterns.
This commit is contained in:
@@ -1,7 +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 { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest, logSpanToDatabase, startSpan, endSpan } from '../_shared/logger.ts';
|
||||
import { addSpanEvent } from '../_shared/logger.ts';
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
interface EscalationRequest {
|
||||
@@ -10,276 +10,219 @@ interface EscalationRequest {
|
||||
escalatedBy: string;
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext) => {
|
||||
const { submissionId, escalationReason, escalatedBy }: EscalationRequest = await req.json();
|
||||
|
||||
addSpanEvent(span, 'processing_escalation', { submissionId, escalatedBy });
|
||||
|
||||
// Fetch submission details
|
||||
const { data: submission, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('*, profiles:user_id(username, display_name, id)')
|
||||
.eq('id', submissionId)
|
||||
.single();
|
||||
|
||||
if (submissionError || !submission) {
|
||||
throw new Error(`Failed to fetch submission: ${submissionError?.message || 'Not found'}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||
);
|
||||
// Fetch escalator details
|
||||
const { data: escalator, error: escalatorError } = await supabase
|
||||
.from('profiles')
|
||||
.select('username, display_name')
|
||||
.eq('user_id', escalatedBy)
|
||||
.single();
|
||||
|
||||
const { submissionId, escalationReason, escalatedBy }: EscalationRequest = await req.json();
|
||||
if (escalatorError) {
|
||||
addSpanEvent(span, 'escalator_profile_fetch_failed', { error: escalatorError.message });
|
||||
}
|
||||
|
||||
edgeLogger.info('Processing escalation notification', {
|
||||
requestId: tracking.requestId,
|
||||
submissionId,
|
||||
escalatedBy,
|
||||
action: 'send_escalation'
|
||||
});
|
||||
// Fetch submission items count
|
||||
const { count: itemsCount, error: countError } = await supabase
|
||||
.from('submission_items')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('submission_id', submissionId);
|
||||
|
||||
// Fetch submission details
|
||||
const { data: submission, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('*, profiles:user_id(username, display_name, id)')
|
||||
.eq('id', submissionId)
|
||||
.single();
|
||||
if (countError) {
|
||||
addSpanEvent(span, 'items_count_fetch_failed', { error: countError.message });
|
||||
}
|
||||
|
||||
if (submissionError || !submission) {
|
||||
throw new Error(`Failed to fetch submission: ${submissionError?.message || 'Not found'}`);
|
||||
}
|
||||
// Prepare email content
|
||||
const escalatorName = escalator?.display_name || escalator?.username || 'Unknown User';
|
||||
const submitterName = submission.profiles?.display_name || submission.profiles?.username || 'Unknown User';
|
||||
const submissionType = submission.submission_type || 'Unknown';
|
||||
const itemCount = itemsCount || 0;
|
||||
|
||||
// Fetch escalator details
|
||||
const { data: escalator, error: escalatorError } = await supabase
|
||||
.from('profiles')
|
||||
.select('username, display_name')
|
||||
.eq('user_id', escalatedBy)
|
||||
.single();
|
||||
|
||||
if (escalatorError) {
|
||||
edgeLogger.error('Failed to fetch escalator profile', {
|
||||
requestId: tracking.requestId,
|
||||
error: escalatorError.message,
|
||||
escalatedBy
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch submission items count
|
||||
const { count: itemsCount, error: countError } = await supabase
|
||||
.from('submission_items')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('submission_id', submissionId);
|
||||
|
||||
if (countError) {
|
||||
edgeLogger.error('Failed to fetch items count', {
|
||||
requestId: tracking.requestId,
|
||||
error: countError.message,
|
||||
submissionId
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare email content
|
||||
const escalatorName = escalator?.display_name || escalator?.username || 'Unknown User';
|
||||
const submitterName = submission.profiles?.display_name || submission.profiles?.username || 'Unknown User';
|
||||
const submissionType = submission.submission_type || 'Unknown';
|
||||
const itemCount = itemsCount || 0;
|
||||
|
||||
const emailSubject = `🚨 Submission Escalated: ${submissionType} - ID: ${submissionId.substring(0, 8)}`;
|
||||
|
||||
const emailHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
||||
.content { background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
|
||||
.info-row { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
|
||||
.label { font-weight: bold; color: #374151; }
|
||||
.value { color: #1f2937; }
|
||||
.reason { background-color: #fef3c7; padding: 15px; border-left: 4px solid #f59e0b; margin: 15px 0; }
|
||||
.footer { text-align: center; padding: 20px; color: #6b7280; font-size: 14px; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; margin: 15px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 style="margin: 0;">⚠️ Submission Escalated</h1>
|
||||
<p style="margin: 5px 0 0 0;">Admin review required</p>
|
||||
const emailSubject = `🚨 Submission Escalated: ${submissionType} - ID: ${submissionId.substring(0, 8)}`;
|
||||
|
||||
const emailHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
||||
.content { background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
|
||||
.info-row { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
|
||||
.label { font-weight: bold; color: #374151; }
|
||||
.value { color: #1f2937; }
|
||||
.reason { background-color: #fef3c7; padding: 15px; border-left: 4px solid #f59e0b; margin: 15px 0; }
|
||||
.footer { text-align: center; padding: 20px; color: #6b7280; font-size: 14px; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; margin: 15px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 style="margin: 0;">⚠️ Submission Escalated</h1>
|
||||
<p style="margin: 5px 0 0 0;">Admin review required</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="info-row">
|
||||
<span class="label">Submission ID:</span>
|
||||
<span class="value">${submissionId}</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="info-row">
|
||||
<span class="label">Submission ID:</span>
|
||||
<span class="value">${submissionId}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Submission Type:</span>
|
||||
<span class="value">${submissionType}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Items Count:</span>
|
||||
<span class="value">${itemCount}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Submitted By:</span>
|
||||
<span class="value">${submitterName}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Escalated By:</span>
|
||||
<span class="value">${escalatorName}</span>
|
||||
</div>
|
||||
|
||||
<div class="reason">
|
||||
<strong>📝 Escalation Reason:</strong>
|
||||
<p style="margin: 10px 0 0 0;">${escalationReason}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="${Deno.env.get('SUPABASE_URL')?.replace('.supabase.co', '.lovable.app')}/admin" class="button">
|
||||
Review Submission →
|
||||
</a>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Submission Type:</span>
|
||||
<span class="value">${submissionType}</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated notification from your moderation system.</p>
|
||||
<p>Please review and take appropriate action.</p>
|
||||
<div class="info-row">
|
||||
<span class="label">Items Count:</span>
|
||||
<span class="value">${itemCount}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Submitted By:</span>
|
||||
<span class="value">${submitterName}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="label">Escalated By:</span>
|
||||
<span class="value">${escalatorName}</span>
|
||||
</div>
|
||||
|
||||
<div class="reason">
|
||||
<strong>📝 Escalation Reason:</strong>
|
||||
<p style="margin: 10px 0 0 0;">${escalationReason}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="${Deno.env.get('SUPABASE_URL')?.replace('.supabase.co', '.lovable.app')}/admin" class="button">
|
||||
Review Submission →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const emailText = `
|
||||
SUBMISSION ESCALATED - Admin Review Required
|
||||
|
||||
Submission ID: ${submissionId}
|
||||
Submission Type: ${submissionType}
|
||||
Items Count: ${itemCount}
|
||||
Submitted By: ${submitterName}
|
||||
Escalated By: ${escalatorName}
|
||||
|
||||
Escalation Reason:
|
||||
${escalationReason}
|
||||
|
||||
Please review this submission in the admin panel.
|
||||
`;
|
||||
|
||||
// Send email via ForwardEmail API with retry
|
||||
const forwardEmailApiKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
||||
const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS');
|
||||
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS');
|
||||
|
||||
if (!forwardEmailApiKey || !adminEmail || !fromEmail) {
|
||||
throw new Error('Email configuration is incomplete. Please check environment variables.');
|
||||
}
|
||||
|
||||
const emailResult = await withEdgeRetry(
|
||||
async () => {
|
||||
const emailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + btoa(forwardEmailApiKey + ':'),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: fromEmail,
|
||||
to: adminEmail,
|
||||
subject: emailSubject,
|
||||
html: emailHtml,
|
||||
text: emailText,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
let errorText;
|
||||
try {
|
||||
errorText = await emailResponse.text();
|
||||
} catch (parseError) {
|
||||
errorText = 'Unable to parse error response';
|
||||
}
|
||||
|
||||
const error = new Error(`Failed to send email: ${emailResponse.status} - ${errorText}`);
|
||||
(error as any).status = emailResponse.status;
|
||||
throw error;
|
||||
}
|
||||
<div class="footer">
|
||||
<p>This is an automated notification from your moderation system.</p>
|
||||
<p>Please review and take appropriate action.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const result = await emailResponse.json();
|
||||
return result;
|
||||
},
|
||||
{ maxAttempts: 3, baseDelay: 1500, maxDelay: 10000 },
|
||||
tracking.requestId,
|
||||
'send-escalation-email'
|
||||
);
|
||||
edgeLogger.info('Email sent successfully', {
|
||||
requestId: tracking.requestId,
|
||||
emailId: emailResult.id
|
||||
});
|
||||
|
||||
// Update submission with notification status
|
||||
const { error: updateError } = await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
escalated: true,
|
||||
escalated_at: new Date().toISOString(),
|
||||
escalated_by: escalatedBy,
|
||||
escalation_reason: escalationReason
|
||||
})
|
||||
.eq('id', submissionId);
|
||||
|
||||
if (updateError) {
|
||||
edgeLogger.error('Failed to update submission escalation status', {
|
||||
requestId: tracking.requestId,
|
||||
error: updateError.message,
|
||||
submissionId
|
||||
});
|
||||
}
|
||||
|
||||
const duration = endRequest(tracking);
|
||||
edgeLogger.info('Escalation notification sent', {
|
||||
requestId: tracking.requestId,
|
||||
duration,
|
||||
emailId: emailResult.id,
|
||||
submissionId
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Escalation notification sent successfully',
|
||||
emailId: emailResult.id,
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{ headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
} }
|
||||
);
|
||||
} catch (error) {
|
||||
const duration = endRequest(tracking);
|
||||
edgeLogger.error('Error in send-escalation-notification', {
|
||||
requestId: tracking.requestId,
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
const emailText = `
|
||||
SUBMISSION ESCALATED - Admin Review Required
|
||||
|
||||
// Persist error to database for monitoring
|
||||
const errorSpan = startSpan('send-escalation-notification-error', 'SERVER');
|
||||
endSpan(errorSpan, 'error', error);
|
||||
logSpanToDatabase(errorSpan, tracking.requestId);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: 'Failed to send escalation notification',
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{ status: 500, headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
} }
|
||||
);
|
||||
Submission ID: ${submissionId}
|
||||
Submission Type: ${submissionType}
|
||||
Items Count: ${itemCount}
|
||||
Submitted By: ${submitterName}
|
||||
Escalated By: ${escalatorName}
|
||||
|
||||
Escalation Reason:
|
||||
${escalationReason}
|
||||
|
||||
Please review this submission in the admin panel.
|
||||
`;
|
||||
|
||||
// Send email via ForwardEmail API with retry
|
||||
const forwardEmailApiKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
||||
const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS');
|
||||
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS');
|
||||
|
||||
if (!forwardEmailApiKey || !adminEmail || !fromEmail) {
|
||||
throw new Error('Email configuration is incomplete. Please check environment variables.');
|
||||
}
|
||||
});
|
||||
|
||||
addSpanEvent(span, 'sending_escalation_email', { adminEmail });
|
||||
|
||||
const emailResult = await withEdgeRetry(
|
||||
async () => {
|
||||
const emailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + btoa(forwardEmailApiKey + ':'),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: fromEmail,
|
||||
to: adminEmail,
|
||||
subject: emailSubject,
|
||||
html: emailHtml,
|
||||
text: emailText,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
let errorText;
|
||||
try {
|
||||
errorText = await emailResponse.text();
|
||||
} catch {
|
||||
errorText = 'Unable to parse error response';
|
||||
}
|
||||
|
||||
const error = new Error(`Failed to send email: ${emailResponse.status} - ${errorText}`);
|
||||
(error as any).status = emailResponse.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return await emailResponse.json();
|
||||
},
|
||||
{ maxAttempts: 3, baseDelay: 1500, maxDelay: 10000 },
|
||||
requestId,
|
||||
'send-escalation-email'
|
||||
);
|
||||
|
||||
addSpanEvent(span, 'email_sent', { emailId: emailResult.id });
|
||||
|
||||
// Update submission with escalation status
|
||||
const { error: updateError } = await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
escalated: true,
|
||||
escalated_at: new Date().toISOString(),
|
||||
escalated_by: escalatedBy,
|
||||
escalation_reason: escalationReason
|
||||
})
|
||||
.eq('id', submissionId);
|
||||
|
||||
if (updateError) {
|
||||
addSpanEvent(span, 'submission_update_failed', { error: updateError.message });
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'escalation_notification_complete', {
|
||||
emailId: emailResult.id,
|
||||
submissionId
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Escalation notification sent successfully',
|
||||
emailId: emailResult.id,
|
||||
};
|
||||
};
|
||||
|
||||
serve(createEdgeFunction({
|
||||
name: 'send-escalation-notification',
|
||||
requireAuth: false,
|
||||
useServiceRole: true,
|
||||
corsHeaders,
|
||||
enableTracing: true,
|
||||
logRequests: true,
|
||||
}, handler));
|
||||
|
||||
Reference in New Issue
Block a user