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.
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
import bcrypt from 'bcryptjs'
|
|
import { verifyResetToken, invalidateResetToken } from './forgot-password'
|
|
import { getUserById } from './[...nextauth]'
|
|
import { AuthResponse, ResetPasswordInput } from '@/lib/auth/types'
|
|
import { withRateLimit } from '@/lib/auth/withAuth'
|
|
|
|
const BCRYPT_ROUNDS = 12
|
|
|
|
async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse<AuthResponse>
|
|
) {
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({
|
|
success: false,
|
|
message: 'Method not allowed',
|
|
error: 'Only POST requests are accepted',
|
|
})
|
|
}
|
|
|
|
try {
|
|
const { token, password }: ResetPasswordInput = req.body
|
|
|
|
if (!token || !password) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Token and new password are required',
|
|
error: 'VALIDATION_ERROR',
|
|
})
|
|
}
|
|
|
|
// Validate password strength
|
|
if (password.length < 8) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Password must be at least 8 characters long',
|
|
error: 'WEAK_PASSWORD',
|
|
})
|
|
}
|
|
|
|
const hasUpperCase = /[A-Z]/.test(password)
|
|
const hasLowerCase = /[a-z]/.test(password)
|
|
const hasNumbers = /\d/.test(password)
|
|
|
|
if (!hasUpperCase || !hasLowerCase || !hasNumbers) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Password must contain uppercase, lowercase, and numbers',
|
|
error: 'WEAK_PASSWORD',
|
|
})
|
|
}
|
|
|
|
// Verify token
|
|
const payload = verifyResetToken(token)
|
|
if (!payload) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Invalid or expired reset token',
|
|
error: 'INVALID_TOKEN',
|
|
})
|
|
}
|
|
|
|
// Get user
|
|
const user = getUserById(payload.userId)
|
|
if (!user) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'User not found',
|
|
error: 'USER_NOT_FOUND',
|
|
})
|
|
}
|
|
|
|
// Hash new password
|
|
const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS)
|
|
|
|
// Update user password (in-memory for now)
|
|
user.passwordHash = passwordHash
|
|
|
|
// Invalidate the used token
|
|
invalidateResetToken(token)
|
|
|
|
// TODO: Invalidate all existing sessions for this user (security best practice)
|
|
// TODO: Send confirmation email
|
|
|
|
console.log(`Password reset successful for user: ${user.email}`)
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Password reset successful. You can now sign in with your new password.',
|
|
})
|
|
} catch (error) {
|
|
console.error('Reset password error:', error)
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'An error occurred resetting your password',
|
|
error: 'INTERNAL_ERROR',
|
|
})
|
|
}
|
|
}
|
|
|
|
// Apply rate limiting: 5 requests per minute per IP
|
|
export default withRateLimit(handler, {
|
|
limit: 5,
|
|
windowMs: 60000,
|
|
})
|