feat: add comprehensive authentication system with JWT and OAuth
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>
This commit is contained in:
2025-11-25 04:37:05 +00:00
parent 11709da8fa
commit 2d53b9e283
17 changed files with 6264 additions and 139 deletions

View File

@@ -8,32 +8,106 @@ datasource db {
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
username String? @unique
passwordHash String? // null for OAuth-only users
name String?
avatar String?
provider String @default("local") // "local", "google", "github", etc.
providerId String? // OAuth provider's user ID
role Role @default(USER)
emailVerified Boolean @default(false)
emailVerifiedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
recipes Recipe[]
cookbooks Cookbook[]
sharedRecipes RecipeShare[]
refreshTokens RefreshToken[]
verificationTokens VerificationToken[]
@@index([email])
@@index([provider, providerId])
}
model VerificationToken {
id String @id @default(cuid())
userId String
token String @unique
type TokenType
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([token])
}
enum TokenType {
EMAIL_VERIFICATION
PASSWORD_RESET
}
enum Role {
USER
ADMIN
}
enum Visibility {
PRIVATE // Only owner can see
SHARED // Owner + specific users
PUBLIC // Everyone can see
}
model RefreshToken {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([token])
}
model Recipe {
id String @id @default(cuid())
id String @id @default(cuid())
title String
description String?
prepTime Int? // minutes
cookTime Int? // minutes
totalTime Int? // minutes
prepTime Int? // minutes
cookTime Int? // minutes
totalTime Int? // minutes
servings Int?
imageUrl String?
sourceUrl String? // For imported recipes
sourceUrl String? // For imported recipes
author String?
cuisine String?
categories String[] @default([]) // Changed from single category to array
categories String[] @default([]) // Changed from single category to array
rating Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String? // Recipe owner
visibility Visibility @default(PRIVATE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
sections RecipeSection[]
ingredients Ingredient[]
instructions Instruction[]
images RecipeImage[]
tags RecipeTag[]
cookbooks CookbookRecipe[]
sharedWith RecipeShare[]
@@index([title])
@@index([cuisine])
@@index([userId])
@@index([visibility])
}
model RecipeSection {
@@ -128,19 +202,36 @@ model RecipeTag {
@@index([tagId])
}
model RecipeShare {
id String @id @default(cuid())
recipeId String
userId String
createdAt DateTime @default(now())
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([recipeId, userId])
@@index([recipeId])
@@index([userId])
}
model Cookbook {
id String @id @default(cuid())
name String
description String?
coverImageUrl String?
userId String? // Cookbook owner
autoFilterCategories String[] @default([]) // Auto-add recipes matching these categories
autoFilterTags String[] @default([]) // Auto-add recipes matching these tags
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
recipes CookbookRecipe[]
@@index([name])
@@index([userId])
}
model CookbookRecipe {