Files
basil/packages/api/src/services/email.service.ts
Paul R Kartchner 2d53b9e283
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
feat: add comprehensive authentication system with JWT and OAuth
Implement a complete authentication system with local email/password
authentication, Google OAuth, JWT tokens, and role-based access control.

Backend Features:
- Database schema with User, RefreshToken, VerificationToken, RecipeShare models
- Role-based access control (USER, ADMIN)
- Recipe visibility controls (PRIVATE, SHARED, PUBLIC)
- Email verification for local accounts
- Password reset functionality
- JWT access tokens (15min) and refresh tokens (7 days)
- Passport.js strategies: Local, JWT, Google OAuth
- bcrypt password hashing with 12 salt rounds
- Password strength validation (min 8 chars, uppercase, lowercase, number)
- Rate limiting on auth endpoints (5 attempts/15min)
- Email service with styled HTML templates for verification and password reset

API Endpoints:
- POST /api/auth/register - Register with email/password
- POST /api/auth/login - Login and get tokens
- POST /api/auth/logout - Invalidate refresh token
- POST /api/auth/refresh - Get new access token
- GET /api/auth/verify-email/:token - Verify email address
- POST /api/auth/resend-verification - Resend verification email
- POST /api/auth/forgot-password - Request password reset
- POST /api/auth/reset-password - Reset password with token
- GET /api/auth/google - Initiate Google OAuth
- GET /api/auth/google/callback - Google OAuth callback
- GET /api/auth/me - Get current user info

Security Middleware:
- requireAuth - Protect routes requiring authentication
- requireAdmin - Admin-only route protection
- optionalAuth - Routes that work with or without auth
- requireOwnership - Check resource ownership

Admin Tools:
- npm run create-admin - Interactive script to create admin users
- verify-user-manual.ts - Helper script for testing

Test Coverage:
- 49 unit and integration tests (all passing)
- Password utility tests (12 tests)
- JWT utility tests (17 tests)
- Auth middleware tests (12 tests)
- Auth routes integration tests (8 tests)

Dependencies Added:
- passport, passport-local, passport-jwt, passport-google-oauth20
- bcrypt, jsonwebtoken
- nodemailer
- express-rate-limit, express-validator, cookie-parser

Environment Variables Required:
- JWT_SECRET, JWT_REFRESH_SECRET
- JWT_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN
- GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (optional)
- SMTP configuration for email
- APP_URL, API_URL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 04:37:05 +00:00

184 lines
6.2 KiB
TypeScript

import nodemailer from 'nodemailer';
const SMTP_HOST = process.env.SMTP_HOST || 'smtp.gmail.com';
const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587');
const SMTP_SECURE = process.env.SMTP_SECURE === 'true';
const SMTP_USER = process.env.SMTP_USER;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD;
const EMAIL_FROM = process.env.EMAIL_FROM || 'Basil Recipe Manager <noreply@basil.local>';
const APP_URL = process.env.APP_URL || 'http://localhost:5173';
// Create transporter
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_SECURE,
auth: SMTP_USER && SMTP_PASSWORD ? {
user: SMTP_USER,
pass: SMTP_PASSWORD,
} : undefined,
});
/**
* Send email verification email
*/
export async function sendVerificationEmail(email: string, token: string, name?: string): Promise<void> {
const verificationUrl = `${APP_URL}/verify-email/${token}`;
const htmlContent = `
<!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: #2d5016; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; padding: 12px 30px; background-color: #2d5016; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
.footer { margin-top: 20px; font-size: 12px; color: #666; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌿 Basil Recipe Manager</h1>
</div>
<div class="content">
<h2>Welcome${name ? `, ${name}` : ''}!</h2>
<p>Thank you for registering with Basil Recipe Manager. To complete your registration and verify your email address, please click the button below:</p>
<div style="text-align: center;">
<a href="${verificationUrl}" class="button">Verify Email Address</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #666;">${verificationUrl}</p>
<p><strong>This link will expire in 24 hours.</strong></p>
<p>If you didn't create an account with Basil, you can safely ignore this email.</p>
</div>
<div class="footer">
<p>Basil Recipe Manager - Your Personal Recipe Collection</p>
</div>
</div>
</body>
</html>
`;
const textContent = `
Welcome to Basil Recipe Manager!
${name ? `Hi ${name},` : 'Hello,'}
Thank you for registering. Please verify your email address by clicking the link below:
${verificationUrl}
This link will expire in 24 hours.
If you didn't create an account with Basil, you can safely ignore this email.
---
Basil Recipe Manager
`;
try {
await transporter.sendMail({
from: EMAIL_FROM,
to: email,
subject: 'Verify Your Email - Basil Recipe Manager',
text: textContent,
html: htmlContent,
});
} catch (error) {
console.error('Failed to send verification email:', error);
throw new Error('Failed to send verification email');
}
}
/**
* Send password reset email
*/
export async function sendPasswordResetEmail(email: string, token: string, name?: string): Promise<void> {
const resetUrl = `${APP_URL}/reset-password/${token}`;
const htmlContent = `
<!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: #2d5016; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; padding: 12px 30px; background-color: #2d5016; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
.footer { margin-top: 20px; font-size: 12px; color: #666; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌿 Basil Recipe Manager</h1>
</div>
<div class="content">
<h2>Password Reset Request</h2>
<p>${name ? `Hi ${name},` : 'Hello,'}</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<div style="text-align: center;">
<a href="${resetUrl}" class="button">Reset Password</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #666;">${resetUrl}</p>
<p><strong>This link will expire in 1 hour.</strong></p>
<p>If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>
</div>
<div class="footer">
<p>Basil Recipe Manager - Your Personal Recipe Collection</p>
</div>
</div>
</body>
</html>
`;
const textContent = `
Password Reset Request
${name ? `Hi ${name},` : 'Hello,'}
We received a request to reset your password. Click the link below to create a new password:
${resetUrl}
This link will expire in 1 hour.
If you didn't request a password reset, you can safely ignore this email.
---
Basil Recipe Manager
`;
try {
await transporter.sendMail({
from: EMAIL_FROM,
to: email,
subject: 'Password Reset - Basil Recipe Manager',
text: textContent,
html: htmlContent,
});
} catch (error) {
console.error('Failed to send password reset email:', error);
throw new Error('Failed to send password reset email');
}
}
/**
* Test email configuration
*/
export async function testEmailConfig(): Promise<boolean> {
try {
await transporter.verify();
console.log('✓ Email configuration is valid');
return true;
} catch (error) {
console.error('✗ Email configuration error:', error);
return false;
}
}