Replace Resend with self-hosted email relay for all email sending
- cryptidAuth.ts: sendEmail() now calls email-relay.jeffemmett.com instead of api.resend.com - boardPermissions.ts: admin request emails use email relay - types.ts: RESEND_API_KEY → EMAIL_RELAY_URL + EMAIL_RELAY_API_KEY - wrangler.toml: updated secrets documentation - Tests updated with new mock env vars Email relay is a lightweight Flask service on Netcup that accepts HTTP POST and sends via Mailcow SMTP. Needed because CF Workers can't do TCP/SMTP directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dea24bde81
commit
1d95d1f398
|
|
@ -75,7 +75,8 @@ function createMockEnv(overrides: Partial<Environment> = {}): Environment {
|
||||||
DAILY_API_KEY: 'mock-daily-key',
|
DAILY_API_KEY: 'mock-daily-key',
|
||||||
DAILY_DOMAIN: 'mock.daily.co',
|
DAILY_DOMAIN: 'mock.daily.co',
|
||||||
CRYPTID_DB: createMockD1() as unknown as D1Database,
|
CRYPTID_DB: createMockD1() as unknown as D1Database,
|
||||||
RESEND_API_KEY: 'mock-resend-key',
|
EMAIL_RELAY_URL: 'https://email-relay.jeffemmett.com',
|
||||||
|
EMAIL_RELAY_API_KEY: 'mock-relay-key',
|
||||||
APP_URL: 'https://test.example.com',
|
APP_URL: 'https://test.example.com',
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ function createMockEnv(overrides: Partial<Environment> = {}): Environment {
|
||||||
DAILY_API_KEY: 'mock-daily-key',
|
DAILY_API_KEY: 'mock-daily-key',
|
||||||
DAILY_DOMAIN: 'mock.daily.co',
|
DAILY_DOMAIN: 'mock.daily.co',
|
||||||
CRYPTID_DB: createMockD1() as unknown as D1Database,
|
CRYPTID_DB: createMockD1() as unknown as D1Database,
|
||||||
RESEND_API_KEY: 'mock-resend-key',
|
EMAIL_RELAY_URL: 'https://email-relay.jeffemmett.com',
|
||||||
|
EMAIL_RELAY_API_KEY: 'mock-relay-key',
|
||||||
APP_URL: 'https://test.example.com',
|
APP_URL: 'https://test.example.com',
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1099,13 +1099,13 @@ export async function handleRequestAdminAccess(
|
||||||
const body = await request.json().catch(() => ({})) as { reason?: string };
|
const body = await request.json().catch(() => ({})) as { reason?: string };
|
||||||
|
|
||||||
// Send email to global admin (jeffemmett@gmail.com)
|
// Send email to global admin (jeffemmett@gmail.com)
|
||||||
if (env.RESEND_API_KEY) {
|
if (env.EMAIL_RELAY_URL && env.EMAIL_RELAY_API_KEY) {
|
||||||
const emailFrom = env.CRYPTID_EMAIL_FROM || 'Canvas <noreply@jeffemmett.com>';
|
const emailFrom = env.CRYPTID_EMAIL_FROM || 'Canvas <noreply@jeffemmett.com>';
|
||||||
|
|
||||||
const emailResponse = await fetch('https://api.resend.com/emails', {
|
const emailResponse = await fetch(`${env.EMAIL_RELAY_URL}/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
|
'Authorization': `Bearer ${env.EMAIL_RELAY_API_KEY}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ function generateUUID(): string {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email via Resend
|
// Send email via self-hosted email relay (Mailcow SMTP)
|
||||||
async function sendEmail(
|
async function sendEmail(
|
||||||
env: Environment,
|
env: Environment,
|
||||||
to: string,
|
to: string,
|
||||||
|
|
@ -20,15 +20,17 @@ async function sendEmail(
|
||||||
htmlContent: string
|
htmlContent: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (!env.RESEND_API_KEY) {
|
const relayUrl = env.EMAIL_RELAY_URL;
|
||||||
console.error('RESEND_API_KEY not configured');
|
const relayKey = env.EMAIL_RELAY_API_KEY;
|
||||||
|
if (!relayUrl || !relayKey) {
|
||||||
|
console.error('EMAIL_RELAY_URL or EMAIL_RELAY_API_KEY not configured');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('https://api.resend.com/emails', {
|
const response = await fetch(`${relayUrl}/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
|
'Authorization': `Bearer ${relayKey}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -41,7 +43,7 @@ async function sendEmail(
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Resend error:', errorText);
|
console.error('Email relay error:', errorText);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ export interface Environment {
|
||||||
DAILY_DOMAIN: string;
|
DAILY_DOMAIN: string;
|
||||||
// CryptID auth bindings
|
// CryptID auth bindings
|
||||||
CRYPTID_DB?: D1Database;
|
CRYPTID_DB?: D1Database;
|
||||||
RESEND_API_KEY?: string;
|
EMAIL_RELAY_URL?: string;
|
||||||
|
EMAIL_RELAY_API_KEY?: string;
|
||||||
CRYPTID_EMAIL_FROM?: string;
|
CRYPTID_EMAIL_FROM?: string;
|
||||||
APP_URL?: string;
|
APP_URL?: string;
|
||||||
// Admin secret for protected endpoints
|
// Admin secret for protected endpoints
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ crons = ["0 0 * * *"] # Run at midnight UTC every day
|
||||||
# - CLOUDFLARE_API_TOKEN
|
# - CLOUDFLARE_API_TOKEN
|
||||||
# - FAL_API_KEY # For fal.ai image/video generation proxy
|
# - FAL_API_KEY # For fal.ai image/video generation proxy
|
||||||
# - RUNPOD_API_KEY # For RunPod AI endpoints proxy
|
# - RUNPOD_API_KEY # For RunPod AI endpoints proxy
|
||||||
# - RESEND_API_KEY # For email sending
|
# - EMAIL_RELAY_API_KEY # For email sending via self-hosted relay
|
||||||
# - ADMIN_SECRET # For admin-only endpoints
|
# - ADMIN_SECRET # For admin-only endpoints
|
||||||
#
|
#
|
||||||
# To set secrets:
|
# To set secrets:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue