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
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>
184 lines
6.2 KiB
TypeScript
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;
|
|
}
|
|
}
|