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.
191 lines
5.1 KiB
TypeScript
191 lines
5.1 KiB
TypeScript
import NextAuth, { NextAuthOptions } from 'next-auth'
|
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
|
import GitHubProvider from 'next-auth/providers/github'
|
|
import GoogleProvider from 'next-auth/providers/google'
|
|
import bcrypt from 'bcryptjs'
|
|
import { UserRole } from '@/lib/auth/types'
|
|
|
|
// In-memory user store for MVP (will be replaced with Prisma in Agent 2)
|
|
// This simulates database operations
|
|
interface StoredUser {
|
|
id: string
|
|
email: string
|
|
name: string | null
|
|
passwordHash: string | null
|
|
role: UserRole
|
|
emailVerified: Date | null
|
|
image: string | null
|
|
createdAt: Date
|
|
}
|
|
|
|
// Temporary in-memory store (will be replaced with database)
|
|
const users: Map<string, StoredUser> = new Map()
|
|
|
|
// Helper to find user by email
|
|
function findUserByEmail(email: string): StoredUser | undefined {
|
|
for (const user of users.values()) {
|
|
if (user.email.toLowerCase() === email.toLowerCase()) {
|
|
return user
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
// Helper to create user
|
|
export function createUser(data: {
|
|
email: string
|
|
name?: string
|
|
passwordHash?: string
|
|
role?: UserRole
|
|
}): StoredUser {
|
|
const id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
const user: StoredUser = {
|
|
id,
|
|
email: data.email.toLowerCase(),
|
|
name: data.name || null,
|
|
passwordHash: data.passwordHash || null,
|
|
role: data.role || UserRole.USER,
|
|
emailVerified: null,
|
|
image: null,
|
|
createdAt: new Date(),
|
|
}
|
|
users.set(id, user)
|
|
return user
|
|
}
|
|
|
|
// Helper to get user by ID
|
|
export function getUserById(id: string): StoredUser | undefined {
|
|
return users.get(id)
|
|
}
|
|
|
|
// Helper to verify password
|
|
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
return bcrypt.compare(password, hash)
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
providers: [
|
|
CredentialsProvider({
|
|
name: 'Credentials',
|
|
credentials: {
|
|
email: { label: 'Email', type: 'email', placeholder: 'your@email.com' },
|
|
password: { label: 'Password', type: 'password' },
|
|
},
|
|
async authorize(credentials) {
|
|
if (!credentials?.email || !credentials?.password) {
|
|
throw new Error('Email and password are required')
|
|
}
|
|
|
|
const user = findUserByEmail(credentials.email)
|
|
|
|
if (!user) {
|
|
throw new Error('No user found with this email')
|
|
}
|
|
|
|
if (!user.passwordHash) {
|
|
throw new Error('Please sign in with your OAuth provider')
|
|
}
|
|
|
|
const isValid = await verifyPassword(credentials.password, user.passwordHash)
|
|
|
|
if (!isValid) {
|
|
throw new Error('Invalid password')
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
emailVerified: user.emailVerified,
|
|
image: user.image,
|
|
}
|
|
},
|
|
}),
|
|
...(process.env.GITHUB_ID && process.env.GITHUB_SECRET
|
|
? [
|
|
GitHubProvider({
|
|
clientId: process.env.GITHUB_ID,
|
|
clientSecret: process.env.GITHUB_SECRET,
|
|
}),
|
|
]
|
|
: []),
|
|
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
|
? [
|
|
GoogleProvider({
|
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
}),
|
|
]
|
|
: []),
|
|
],
|
|
session: {
|
|
strategy: 'jwt',
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
},
|
|
jwt: {
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
},
|
|
pages: {
|
|
signIn: '/auth/signin',
|
|
signOut: '/auth/signout',
|
|
error: '/auth/error',
|
|
verifyRequest: '/auth/verify-request',
|
|
newUser: '/auth/new-user',
|
|
},
|
|
callbacks: {
|
|
async signIn({ user, account }) {
|
|
// Handle OAuth sign-in
|
|
if (account?.provider !== 'credentials') {
|
|
const existingUser = findUserByEmail(user.email!)
|
|
if (!existingUser) {
|
|
// Create new user for OAuth sign-in
|
|
createUser({
|
|
email: user.email!,
|
|
name: user.name || undefined,
|
|
role: UserRole.USER,
|
|
})
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
async jwt({ token, user, trigger, session }) {
|
|
// Initial sign-in
|
|
if (user) {
|
|
token.id = user.id
|
|
token.role = user.role || UserRole.USER
|
|
token.emailVerified = user.emailVerified
|
|
}
|
|
|
|
// Handle session update
|
|
if (trigger === 'update' && session) {
|
|
token.name = session.name
|
|
token.role = session.role
|
|
}
|
|
|
|
return token
|
|
},
|
|
async session({ session, token }) {
|
|
if (session.user) {
|
|
session.user.id = token.id
|
|
session.user.role = token.role
|
|
session.user.emailVerified = token.emailVerified
|
|
}
|
|
return session
|
|
},
|
|
},
|
|
events: {
|
|
async signIn({ user, isNewUser }) {
|
|
console.log(`User signed in: ${user.email}, isNewUser: ${isNewUser}`)
|
|
},
|
|
async signOut({ token }) {
|
|
console.log(`User signed out: ${token.email}`)
|
|
},
|
|
},
|
|
debug: process.env.NODE_ENV === 'development',
|
|
}
|
|
|
|
export default NextAuth(authOptions)
|
|
|
|
// Export helper for use in registration API
|
|
export { findUserByEmail, verifyPassword }
|