- Fork elmeg-demo codebase for multi-band support - Add data importer infrastructure with base class - Create band-specific importers: - phish.py: Phish.net API v5 - grateful_dead.py: Grateful Stats API - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm) - Add spec-kit configuration for Gemini - Update README with supported bands and architecture
209 lines
5.6 KiB
TypeScript
209 lines
5.6 KiB
TypeScript
/**
|
|
* Elmeg Email Service - AWS SES v2 Integration
|
|
*
|
|
* Transactional email layer for user-initiated emails only.
|
|
* Uses AWS SES stored templates for consistent, reliable delivery.
|
|
*/
|
|
|
|
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
|
|
// Configuration from environment variables
|
|
const config = {
|
|
region: process.env.AWS_SES_REGION || "us-east-1",
|
|
fromAddress: process.env.EMAIL_FROM || "noreply@elmeg.xyz",
|
|
appName: "Elmeg",
|
|
supportEmail: process.env.SUPPORT_EMAIL || "support@elmeg.xyz",
|
|
frontendUrl: process.env.FRONTEND_URL || "https://elmeg.xyz",
|
|
};
|
|
|
|
// SES Template Names
|
|
export const SES_TEMPLATES = {
|
|
EMAIL_VERIFICATION: "ELMEG_EMAIL_VERIFICATION",
|
|
PASSWORD_RESET: "ELMEG_PASSWORD_RESET",
|
|
SECURITY_ALERT: "ELMEG_SECURITY_ALERT",
|
|
} as const;
|
|
|
|
// Initialize SES v2 client
|
|
const sesClient = new SESv2Client({
|
|
region: config.region,
|
|
// Credentials are loaded automatically from env vars:
|
|
// AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
|
});
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
export interface SendVerificationEmailParams {
|
|
to: string;
|
|
userName: string;
|
|
verificationLink: string;
|
|
}
|
|
|
|
export interface SendPasswordResetEmailParams {
|
|
to: string;
|
|
userName: string;
|
|
resetLink: string;
|
|
}
|
|
|
|
export interface SendSecurityAlertEmailParams {
|
|
to: string;
|
|
userName: string;
|
|
securityEventDescription: string;
|
|
}
|
|
|
|
export interface EmailResult {
|
|
success: boolean;
|
|
messageId?: string;
|
|
error?: {
|
|
code: string;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
export class EmailError extends Error {
|
|
code: string;
|
|
|
|
constructor(code: string, message: string) {
|
|
super(message);
|
|
this.name = "EmailError";
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Email Sending Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Send email verification email to new users
|
|
*/
|
|
export async function sendVerificationEmail(
|
|
params: SendVerificationEmailParams
|
|
): Promise<EmailResult> {
|
|
const templateData = {
|
|
user_name: params.userName,
|
|
verification_link: params.verificationLink,
|
|
app_name: config.appName,
|
|
support_email: config.supportEmail,
|
|
};
|
|
|
|
return sendTemplatedEmail(
|
|
params.to,
|
|
SES_TEMPLATES.EMAIL_VERIFICATION,
|
|
templateData
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send password reset email
|
|
*/
|
|
export async function sendPasswordResetEmail(
|
|
params: SendPasswordResetEmailParams
|
|
): Promise<EmailResult> {
|
|
const templateData = {
|
|
user_name: params.userName,
|
|
reset_link: params.resetLink,
|
|
app_name: config.appName,
|
|
support_email: config.supportEmail,
|
|
};
|
|
|
|
return sendTemplatedEmail(
|
|
params.to,
|
|
SES_TEMPLATES.PASSWORD_RESET,
|
|
templateData
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send security alert email for account events
|
|
*/
|
|
export async function sendSecurityAlertEmail(
|
|
params: SendSecurityAlertEmailParams
|
|
): Promise<EmailResult> {
|
|
const templateData = {
|
|
user_name: params.userName,
|
|
security_event_description: params.securityEventDescription,
|
|
app_name: config.appName,
|
|
support_email: config.supportEmail,
|
|
};
|
|
|
|
return sendTemplatedEmail(
|
|
params.to,
|
|
SES_TEMPLATES.SECURITY_ALERT,
|
|
templateData
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Core Email Function
|
|
// =============================================================================
|
|
|
|
async function sendTemplatedEmail(
|
|
to: string,
|
|
templateName: string,
|
|
templateData: Record<string, string>
|
|
): Promise<EmailResult> {
|
|
try {
|
|
const command = new SendEmailCommand({
|
|
FromEmailAddress: config.fromAddress,
|
|
Destination: {
|
|
ToAddresses: [to],
|
|
},
|
|
Content: {
|
|
Template: {
|
|
TemplateName: templateName,
|
|
TemplateData: JSON.stringify(templateData),
|
|
},
|
|
},
|
|
});
|
|
|
|
const response = await sesClient.send(command);
|
|
|
|
return {
|
|
success: true,
|
|
messageId: response.MessageId,
|
|
};
|
|
} catch (error: unknown) {
|
|
const err = error as { name?: string; message?: string; Code?: string };
|
|
|
|
console.error(`[Email] Failed to send ${templateName} to ${to}:`, err.message);
|
|
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: err.Code || err.name || "UNKNOWN_ERROR",
|
|
message: err.message || "Failed to send email",
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Utility Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Generate a verification link for a user
|
|
*/
|
|
export function generateVerificationLink(token: string): string {
|
|
return `${config.frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
|
|
}
|
|
|
|
/**
|
|
* Generate a password reset link for a user
|
|
*/
|
|
export function generateResetLink(token: string): string {
|
|
return `${config.frontendUrl}/reset-password?token=${encodeURIComponent(token)}`;
|
|
}
|
|
|
|
/**
|
|
* Check if the email service is properly configured
|
|
*/
|
|
export function isEmailConfigured(): boolean {
|
|
return !!(
|
|
process.env.AWS_ACCESS_KEY_ID &&
|
|
process.env.AWS_SECRET_ACCESS_KEY &&
|
|
process.env.AWS_SES_REGION
|
|
);
|
|
}
|