Your commit message here

This commit is contained in:
Chop
2025-06-25 00:22:12 +02:00
parent 4b2a544870
commit 035a0386d7
16 changed files with 1091 additions and 19 deletions

173
src/lib/auth.js Normal file
View 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)