Add complete user authentication with NextAuth.js supporting: - Email/password credentials authentication - OAuth providers (GitHub, Google) with optional configuration - JWT-based session management with 30-day expiry - Role-based access control (USER, GROWER, FARM_MANAGER, ADMIN) - Permission system with granular access control - Secure password hashing with bcrypt (12 rounds) - Rate limiting on auth endpoints - Password reset flow with secure tokens - Email verification system Files added: - lib/auth/: Core auth library (types, permissions, context, hooks, middleware) - pages/api/auth/: Auth API routes (NextAuth, register, forgot-password, verify-email) - pages/auth/: Auth pages (signin, signup, forgot-password, reset-password, verify-email) - components/auth/: Reusable auth components (LoginForm, RegisterForm, AuthGuard, etc.) Updated _app.tsx to include SessionProvider for auth state management.
151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
import crypto from 'crypto'
|
|
import { findUserByEmail, getUserById } from './[...nextauth]'
|
|
import { AuthResponse, VerifyEmailInput, TokenPayload } from '@/lib/auth/types'
|
|
import { withRateLimit } from '@/lib/auth/withAuth'
|
|
|
|
// In-memory token store (will be replaced with database in Agent 2)
|
|
const emailVerificationTokens = new Map<string, TokenPayload>()
|
|
|
|
// Token expiry: 24 hours
|
|
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000
|
|
|
|
export function generateVerificationToken(userId: string, email: string): string {
|
|
const token = crypto.randomBytes(32).toString('hex')
|
|
const payload: TokenPayload = {
|
|
userId,
|
|
email,
|
|
type: 'email_verification',
|
|
expiresAt: Date.now() + TOKEN_EXPIRY_MS,
|
|
}
|
|
emailVerificationTokens.set(token, payload)
|
|
return token
|
|
}
|
|
|
|
function verifyToken(token: string): TokenPayload | null {
|
|
const payload = emailVerificationTokens.get(token)
|
|
if (!payload) return null
|
|
if (Date.now() > payload.expiresAt) {
|
|
emailVerificationTokens.delete(token)
|
|
return null
|
|
}
|
|
return payload
|
|
}
|
|
|
|
function invalidateToken(token: string): void {
|
|
emailVerificationTokens.delete(token)
|
|
}
|
|
|
|
async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse<AuthResponse>
|
|
) {
|
|
// Handle GET request (verify token from email link)
|
|
if (req.method === 'GET') {
|
|
const { token } = req.query
|
|
|
|
if (!token || typeof token !== 'string') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Verification token is required',
|
|
error: 'VALIDATION_ERROR',
|
|
})
|
|
}
|
|
|
|
const payload = verifyToken(token)
|
|
if (!payload) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Invalid or expired verification token',
|
|
error: 'INVALID_TOKEN',
|
|
})
|
|
}
|
|
|
|
const user = getUserById(payload.userId)
|
|
if (!user) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'User not found',
|
|
error: 'USER_NOT_FOUND',
|
|
})
|
|
}
|
|
|
|
// Mark email as verified
|
|
user.emailVerified = new Date()
|
|
|
|
// Invalidate the used token
|
|
invalidateToken(token)
|
|
|
|
console.log(`Email verified for user: ${user.email}`)
|
|
|
|
// Redirect to success page or return success response
|
|
if (req.headers.accept?.includes('text/html')) {
|
|
res.redirect(302, '/auth/email-verified')
|
|
return
|
|
}
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Email verified successfully',
|
|
})
|
|
}
|
|
|
|
// Handle POST request (resend verification email)
|
|
if (req.method === 'POST') {
|
|
const { email } = req.body
|
|
|
|
if (!email) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Email is required',
|
|
error: 'VALIDATION_ERROR',
|
|
})
|
|
}
|
|
|
|
const user = findUserByEmail(email)
|
|
if (!user) {
|
|
// Return success to prevent email enumeration
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'If an account exists with this email, a verification link has been sent.',
|
|
})
|
|
}
|
|
|
|
if (user.emailVerified) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Email is already verified',
|
|
error: 'ALREADY_VERIFIED',
|
|
})
|
|
}
|
|
|
|
// Generate new verification token
|
|
const verificationToken = generateVerificationToken(user.id, user.email)
|
|
|
|
// Build verification URL
|
|
const baseUrl = process.env.NEXTAUTH_URL || `http://${req.headers.host}`
|
|
const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${verificationToken}`
|
|
|
|
// TODO: Send verification email (will be implemented with Agent 8 - Notifications)
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`Email verification link for ${email}: ${verifyUrl}`)
|
|
}
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'If an account exists with this email, a verification link has been sent.',
|
|
})
|
|
}
|
|
|
|
return res.status(405).json({
|
|
success: false,
|
|
message: 'Method not allowed',
|
|
error: 'Only GET and POST requests are accepted',
|
|
})
|
|
}
|
|
|
|
// Apply rate limiting: 3 requests per minute per IP
|
|
export default withRateLimit(handler, {
|
|
limit: 3,
|
|
windowMs: 60000,
|
|
})
|