Add master template wrapper to OTP emails

- Add EmailTemplateRenderer class to send-auth-otp edge function
- Wrap OTP email content in master template with brutalist design
- Email now includes proper header, footer, and styling
- No changes needed to checkout flow (uses auth page for registration)

Benefits:
- Professional branded emails with ACCESS HUB header
- Consistent brutalist design across all emails
- Responsive layout
- Better email client compatibility
This commit is contained in:
dwindown
2026-01-02 15:19:41 +07:00
parent 967829b612
commit bd3841b716
2 changed files with 420 additions and 3 deletions

156
DEPLOY-OTP-FIX.md Normal file
View File

@@ -0,0 +1,156 @@
# Deploy OTP Email Fix
## Problem
The `send-auth-otp` edge function was trying to insert into `notification_logs` table which doesn't exist, causing the function to crash AFTER sending the email. This meant:
- ✅ Email was sent by Mailketing API
- ❌ Function crashed before returning success
- ❌ Frontend might have shown error
## Solution
Removed all references to `notification_logs` table from the edge function.
## Deployment Steps
### 1. SSH into your server
```bash
ssh root@lovable.backoffice.biz.id
```
### 2. Navigate to the project directory
```bash
cd /path/to/your/project
```
### 3. Pull the latest changes
```bash
git pull origin main
```
### 4. Deploy the edge function
```bash
# Option A: If using Supabase CLI
supabase functions deploy send-auth-otp
# Option B: If manually copying files
cp supabase/functions/send-auth-otp/index.ts /path/to/supabase/functions/send-auth-otp/index.ts
# Then restart the edge function container
docker-compose restart edge-functions
# or
docker restart $(docker ps -q --filter 'name=supabase_edge_runtime')
```
### 5. Verify deployment
```bash
# Check if function is loaded
supabase functions list
# Should show:
# send-auth-otp ...
# verify-auth-otp ...
# send-email-v2 ...
```
### 6. Test the fix
```bash
# Test with curl
curl -X POST https://lovable.backoffice.biz.id/functions/v1/send-auth-otp \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id":"TEST_USER_ID","email":"test@example.com"}'
# Expected response:
# {"success":true,"message":"OTP sent successfully"}
```
### 7. Test full registration flow
1. Open browser to https://with.dwindi.com/auth
2. Register with new email
3. Check email inbox
4. Should receive OTP code
## What Changed
### File: `supabase/functions/send-auth-otp/index.ts`
**Before:**
```typescript
// Log notification
await supabase
.from('notification_logs')
.insert({
user_id,
email: email,
notification_type: 'auth_email_verification',
status: 'sent',
provider: 'mailketing',
error_message: null,
});
```
**After:**
```typescript
// Note: notification_logs table doesn't exist, skipping logging
```
## Troubleshooting
### If email still not received:
1. **Check edge function logs:**
```bash
docker logs $(docker ps -q --filter 'name=supabase_edge_runtime') | tail -50
```
2. **Check if OTP was created:**
```sql
SELECT * FROM auth_otps ORDER BY created_at DESC LIMIT 1;
```
3. **Check notification settings:**
```sql
SELECT platform_name, from_name, from_email, api_token
FROM notification_settings
LIMIT 1;
```
4. **Verify email template:**
```sql
SELECT key, name, is_active, LENGTH(email_body_html) as html_length
FROM notification_templates
WHERE key = 'auth_email_verification';
```
5. **Test email sending directly:**
```bash
curl -X POST https://lovable.backoffice.biz.id/functions/v1/send-email-v2 \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "your@email.com",
"api_token": "YOUR_MAILKETING_TOKEN",
"from_name": "Test",
"from_email": "test@with.dwindi.com",
"subject": "Test Email",
"html_body": "<h1>Test</h1>"
}'
```
## Success Criteria
✅ Edge function returns `{"success":true}`
✅ No crashes in edge function logs
✅ OTP created in database
✅ Email received with OTP code
✅ OTP verification works
✅ User can login after verification
## Next Steps
After successful deployment:
1. Test registration with multiple email addresses
2. Test OTP verification flow
3. Test login after verification
4. Test "resend OTP" functionality
5. Test expired OTP (wait 15 minutes)
6. Test wrong OTP code

View File

@@ -11,6 +11,260 @@ interface SendOTPRequest {
email: string;
}
// Email Template Renderer (Master Template)
interface EmailTemplateData {
subject: string;
content: string;
brandName?: string;
brandLogo?: string;
}
class EmailTemplateRenderer {
private static readonly MASTER_TEMPLATE = `
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{subject}}</title>
<style>
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
table { border-collapse: collapse !important; }
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; background-color: #FFFFFF; }
:root {
--color-black: #000000;
--color-white: #FFFFFF;
--color-gray: #F4F4F5;
--color-success: #00A651;
--color-danger: #E11D48;
--border-thick: 2px solid #000000;
--border-thin: 1px solid #000000;
--shadow-hard: 4px 4px 0px 0px #000000;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #000000;
-webkit-font-smoothing: antialiased;
}
.email-content h1 {
font-size: 28px;
font-weight: 800;
margin: 0 0 20px 0;
letter-spacing: -1px;
line-height: 1.1;
}
.email-content h2 {
font-size: 20px;
font-weight: 700;
margin: 25px 0 15px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid #000;
padding-bottom: 5px;
display: inline-block;
}
.email-content h3 {
font-size: 18px;
font-weight: 700;
margin: 20px 0 10px 0;
color: #333;
}
.email-content p {
font-size: 16px;
line-height: 1.6;
margin: 0 0 20px 0;
color: #333;
}
.email-content a {
color: #000000;
text-decoration: underline;
font-weight: 700;
text-underline-offset: 3px;
}
.email-content ul, .email-content ol {
margin: 0 0 20px 0;
padding-left: 20px;
}
.email-content li {
margin-bottom: 8px;
font-size: 16px;
padding-left: 5px;
}
.email-content table {
width: 100%;
border: 2px solid #000;
margin-bottom: 25px;
border-collapse: collapse;
}
.email-content th {
background-color: #000;
color: #FFF;
padding: 12px;
text-align: left;
font-size: 14px;
text-transform: uppercase;
font-weight: 700;
border: 1px solid #000;
}
.email-content td {
padding: 12px;
border: 1px solid #000;
font-size: 15px;
vertical-align: top;
}
.email-content tr:nth-child(even) td {
background-color: #F8F8F8;
}
.btn {
display: inline-block;
background-color: #000;
color: #FFF !important;
padding: 14px 28px;
font-weight: 700;
text-transform: uppercase;
text-decoration: none !important;
font-size: 16px;
border: 2px solid #000;
box-shadow: 4px 4px 0px 0px #000000;
margin: 10px 0;
transition: all 0.1s;
}
.btn:hover {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px 0px #000000;
}
.email-content pre {
background-color: #F4F4F5;
border: 2px solid #000;
padding: 15px;
overflow-x: auto;
margin-bottom: 20px;
}
.email-content code {
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
color: #E11D48;
background-color: #F4F4F5;
padding: 2px 4px;
}
.otp-box {
background-color: #F4F4F5;
border: 2px dashed #000;
padding: 20px;
text-align: center;
margin: 20px 0;
letter-spacing: 5px;
font-family: 'Courier New', Courier, monospace;
font-size: 32px;
font-weight: 700;
color: #000;
}
.email-content blockquote {
margin: 0 0 20px 0;
padding: 15px 20px;
border-left: 6px solid #000;
background-color: #F9F9F9;
font-style: italic;
font-weight: 500;
}
.alert-success { background-color: #E6F4EA; border-left-color: #00A651; color: #005A2B; }
.alert-danger { background-color: #FFE4E6; border-left-color: #E11D48; color: #881337; }
.alert-info { background-color: #E3F2FD; border-left-color: #1976D2; color: #0D47A1; }
@media screen and (max-width: 600px) {
.email-container { width: 100% !important; border-left: 0 !important; border-right: 0 !important; }
.content-padding { padding: 30px 20px !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #FFFFFF;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #FFFFFF;">
<tr>
<td align="center" style="padding: 40px 0;">
<table border="0" cellpadding="0" cellspacing="0" width="600" class="email-container" style="background-color: #FFFFFF; border: 2px solid #000000; width: 600px; min-width: 320px;">
<tr>
<td align="left" style="background-color: #000000; padding: 25px 40px; border-bottom: 2px solid #000000;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="left">
<div style="font-family: 'Helvetica Neue', sans-serif; font-size: 24px; font-weight: 900; color: #FFFFFF; letter-spacing: -1px; text-transform: uppercase;">
{{brandName}}
</div>
</td>
<td align="right">
<div style="font-family: monospace; font-size: 12px; color: #888;">
NOTIF #{{timestamp}}
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content-padding" style="padding: 40px 40px 60px 40px;">
<div class="email-content">
{{content}}
</div>
</td>
</tr>
<tr>
<td style="padding: 30px 40px; border-top: 2px solid #000000; background-color: #F4F4F5; color: #000;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="left" style="font-size: 12px; line-height: 18px; font-family: monospace; color: #555;">
<p style="margin: 0 0 10px 0; font-weight: bold;">{{brandName}}</p>
<p style="margin: 0 0 15px 0;">Email ini dikirim otomatis. Jangan membalas email ini.</p>
<p style="margin: 0;">
<a href="#" style="color: #000; text-decoration: underline;">Ubah Preferensi</a> &nbsp;|&nbsp;
<a href="#" style="color: #000; text-decoration: underline;">Unsubscribe</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
static render(data: EmailTemplateData): string {
let html = this.MASTER_TEMPLATE;
html = html.replace(/{{subject}}/g, data.subject || 'Notification');
html = html.replace(/{{brandName}}/g, data.brandName || 'ACCESS HUB');
html = html.replace(/{{brandLogo}}/g, data.brandLogo || '');
html = html.replace(/{{timestamp}}/g, Date.now().toString().slice(-6));
html = html.replace(/{{content}}/g, data.content);
return html;
}
}
// Generate 6-digit OTP code
function generateOTP(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
@@ -124,10 +378,17 @@ serve(async (req: Request) => {
subject = subject.replace(new RegExp(`{${key}}`, 'g'), value);
});
// Process shortcodes in HTML body
let htmlBody = template.email_body_html;
// Process shortcodes in HTML body content
let htmlContent = template.email_body_html;
Object.entries(templateVars).forEach(([key, value]) => {
htmlBody = htmlBody.replace(new RegExp(`{${key}}`, 'g'), value);
htmlContent = htmlContent.replace(new RegExp(`{${key}}`, 'g'), value);
});
// Wrap in master template
const htmlBody = EmailTemplateRenderer.render({
subject: subject,
content: htmlContent,
brandName: settings.platform_name || 'ACCESS HUB',
});
// Send email via send-email-v2