Your commit message here
This commit is contained in:
173
src/lib/auth.js
Normal file
173
src/lib/auth.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import NextAuth from "next-auth"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import db from "./db.js"
|
||||
import bcrypt from "bcryptjs"
|
||||
import { z } from "zod"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email("Invalid email format"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters")
|
||||
})
|
||||
|
||||
export const authOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
try {
|
||||
// Validate input
|
||||
const validatedFields = loginSchema.parse(credentials)
|
||||
|
||||
// Check if user exists and is active
|
||||
const user = db.prepare(`
|
||||
SELECT id, email, name, password_hash, role, is_active,
|
||||
failed_login_attempts, locked_until
|
||||
FROM users
|
||||
WHERE email = ? AND is_active = 1
|
||||
`).get(validatedFields.email)
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Invalid credentials")
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
||||
throw new Error("Account temporarily locked")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(validatedFields.password, user.password_hash)
|
||||
|
||||
if (!isValidPassword) {
|
||||
// Increment failed attempts
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET failed_login_attempts = failed_login_attempts + 1,
|
||||
locked_until = CASE
|
||||
WHEN failed_login_attempts >= 4
|
||||
THEN datetime('now', '+15 minutes')
|
||||
ELSE locked_until
|
||||
END
|
||||
WHERE id = ?
|
||||
`).run(user.id)
|
||||
|
||||
throw new Error("Invalid credentials")
|
||||
}
|
||||
|
||||
// Reset failed attempts and update last login
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET failed_login_attempts = 0,
|
||||
locked_until = NULL,
|
||||
last_login = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(user.id)
|
||||
|
||||
// Log successful login
|
||||
logAuditEvent(user.id, 'LOGIN_SUCCESS', 'user', user.id, req)
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
updateAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
if (user) {
|
||||
token.role = user.role
|
||||
token.userId = user.id
|
||||
|
||||
// Create session in database
|
||||
const sessionToken = randomBytes(32).toString('hex')
|
||||
const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO sessions (session_token, user_id, expires)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(sessionToken, user.id, expires.toISOString())
|
||||
|
||||
token.sessionToken = sessionToken
|
||||
}
|
||||
return token
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token) {
|
||||
session.user.id = token.userId
|
||||
session.user.role = token.role
|
||||
|
||||
// Verify session is still valid in database
|
||||
const dbSession = db.prepare(`
|
||||
SELECT user_id FROM sessions
|
||||
WHERE session_token = ? AND expires > datetime('now')
|
||||
`).get(token.sessionToken)
|
||||
|
||||
if (!dbSession) {
|
||||
// Session expired or invalid
|
||||
return null
|
||||
}
|
||||
}
|
||||
return session
|
||||
},
|
||||
async signIn({ user, account, profile, email, credentials }) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error'
|
||||
},
|
||||
events: {
|
||||
async signOut({ token }) {
|
||||
// Remove session from database
|
||||
if (token?.sessionToken) {
|
||||
db.prepare(`
|
||||
DELETE FROM sessions WHERE session_token = ?
|
||||
`).run(token.sessionToken)
|
||||
|
||||
if (token.userId) {
|
||||
logAuditEvent(token.userId, 'LOGOUT', 'user', token.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audit logging helper
|
||||
function logAuditEvent(userId, action, resourceType, resourceId, req = null) {
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, ip_address, user_agent)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
userId,
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
req?.ip || req?.socket?.remoteAddress || 'unknown',
|
||||
req?.headers?.['user-agent'] || 'unknown'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Audit log error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAuth(authOptions)
|
||||
Reference in New Issue
Block a user