wulf-pulse/lib/services/email.ts
root 9f912aed24 feat: add authentication, user management, and admin features
Added comprehensive authentication and authorization system:

Authentication System:
- Better Auth integration with session management
- Login/logout pages and API routes
- Middleware for route protection
- Auth utilities and client libraries

User Management:
- User list, detail, and invite pages
- User API endpoints (CRUD operations)
- Session management for users
- Profile settings page

Role-Based Access Control:
- Role management pages (list, create, edit)
- Permission system with granular controls
- Role assignment to users
- Role API endpoints

Admin Features:
- Audit log page for tracking system events
- Admin settings page
- Audit service for logging user actions

Additional Features:
- Quotes management pages and components
- SalesBldr API integration
- Email service for notifications

Configuration & Documentation:
- Updated docker-compose.yml
- MCP server configuration (mcp.json)
- CVE-2025-55182 security review documentation
- Standards guide and PRD documents
- Re-enabling authentication documentation

Database Migrations:
- 012: Auth tables (users, sessions, accounts, verifications)
- 013: Role tables (roles, permissions, role_permissions, user_roles)
- 014: Admin settings table

UI Updates:
- Updated dashboard layout
- Enhanced app layout with auth integration
2026-01-31 12:43:14 -05:00

165 lines
5.8 KiB
TypeScript

import * as nodemailer from "nodemailer";
// SMTP configuration from environment variables
const smtpConfig = {
host: process.env.SMTP_HOST || "smtp.example.com",
port: parseInt(process.env.SMTP_PORT || "587"),
secure: process.env.SMTP_PORT === "465", // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER || "",
pass: process.env.SMTP_PASSWORD || "",
},
};
const fromAddress = process.env.SMTP_FROM || "noreply@example.com";
// Create reusable transporter
let transporter: nodemailer.Transporter | null = null;
function getTransporter(): nodemailer.Transporter {
if (!transporter) {
transporter = nodemailer.createTransport(smtpConfig);
}
return transporter;
}
// Email templates
interface MagicLinkEmailParams {
email: string;
url: string;
token: string;
}
export async function sendMagicLinkEmail({
email,
url,
}: MagicLinkEmailParams): Promise<void> {
const transport = getTransporter();
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign in to Pulse</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Pulse</h1>
</div>
<div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
<h2 style="color: #333; margin-top: 0;">Sign in to your account</h2>
<p>Click the button below to sign in to Pulse. This link will expire in 5 minutes.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${url}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
Sign in to Pulse
</a>
</div>
<p style="color: #666; font-size: 14px;">If you didn't request this email, you can safely ignore it.</p>
<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">
If the button doesn't work, copy and paste this link into your browser:<br>
<a href="${url}" style="color: #667eea; word-break: break-all;">${url}</a>
</p>
</div>
</body>
</html>
`;
const text = `
Sign in to Pulse
Click the link below to sign in to your account. This link will expire in 5 minutes.
${url}
If you didn't request this email, you can safely ignore it.
`;
await transport.sendMail({
from: fromAddress,
to: email,
subject: "Sign in to Pulse",
text,
html,
});
}
interface InvitationEmailParams {
email: string;
inviterName: string;
url: string;
}
export async function sendInvitationEmail({
email,
inviterName,
url,
}: InvitationEmailParams): Promise<void> {
const transport = getTransporter();
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're invited to Pulse</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Pulse</h1>
</div>
<div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
<h2 style="color: #333; margin-top: 0;">You're invited!</h2>
<p><strong>${inviterName}</strong> has invited you to join Pulse.</p>
<p>Click the button below to accept the invitation and set up your account.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${url}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
Accept Invitation
</a>
</div>
<p style="color: #666; font-size: 14px;">If you weren't expecting this invitation, you can safely ignore this email.</p>
<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">
If the button doesn't work, copy and paste this link into your browser:<br>
<a href="${url}" style="color: #667eea; word-break: break-all;">${url}</a>
</p>
</div>
</body>
</html>
`;
const text = `
You're invited to Pulse!
${inviterName} has invited you to join Pulse.
Click the link below to accept the invitation and set up your account:
${url}
If you weren't expecting this invitation, you can safely ignore this email.
`;
await transport.sendMail({
from: fromAddress,
to: email,
subject: "You're invited to Pulse",
text,
html,
});
}
// Verify SMTP connection
export async function verifyEmailConnection(): Promise<boolean> {
try {
const transport = getTransporter();
await transport.verify();
return true;
} catch (error) {
console.error("SMTP connection verification failed:", error);
return false;
}
}