feat(audit-logging): Implement Edge-compatible audit logging utility and safe logging module

- Added `auditLogEdge.js` for Edge Runtime compatible audit logging, including console logging and API fallback.
- Introduced `auditLogSafe.js` for safe audit logging without direct database imports, ensuring compatibility across runtimes.
- Enhanced `auth.js` to integrate safe audit logging for login actions, including success and failure cases.
- Created middleware `auditLog.js` to facilitate audit logging for API routes with predefined configurations.
- Updated `middleware.js` to allow API route access without authentication checks.
- Added tests for audit logging functionality and Edge compatibility in `test-audit-logging.mjs` and `test-edge-compatibility.mjs`.
- Implemented safe audit logging tests in `test-safe-audit-logging.mjs` to verify functionality across environments.
This commit is contained in:
Chop
2025-07-09 23:08:16 +02:00
parent 90875db28b
commit b1a78bf7a8
20 changed files with 2943 additions and 130 deletions

View File

@@ -1,52 +1,60 @@
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
import { z } from "zod"
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Invalid email format"),
password: z.string().min(6, "Password must be at least 6 characters")
})
email: z.string().email("Invalid email format"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
try {
// Import database here to avoid edge runtime issues
const { default: db } = await import("./db.js")
// Validate input
const validatedFields = loginSchema.parse(credentials)
// Check if user exists and is active
const user = db.prepare(`
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
// Import database here to avoid edge runtime issues
const { default: db } = await import("./db.js");
// 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(`
`
)
.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
@@ -55,57 +63,95 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
ELSE locked_until
END
WHERE id = ?
`).run(user.id)
throw new Error("Invalid credentials")
}
// Reset failed attempts and update last login
db.prepare(`
`
).run(user.id);
// Log failed login attempt (only in Node.js runtime)
try {
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } =
await import("./auditLogSafe.js");
await logAuditEventSafe({
action: AUDIT_ACTIONS.LOGIN_FAILED,
userId: user.id,
resourceType: RESOURCE_TYPES.SESSION,
details: {
email: validatedFields.email,
reason: "invalid_password",
failed_attempts: user.failed_login_attempts + 1,
},
});
} catch (auditError) {
console.error("Failed to log audit event:", auditError);
}
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)
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
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.userId = user.id
}
return token
},
async session({ session, token }) {
if (token) {
session.user.id = token.userId
session.user.role = token.role
}
return session
}
},
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error'
},
debug: process.env.NODE_ENV === 'development'
})
`
).run(user.id);
// Log successful login (only in Node.js runtime)
try {
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } =
await import("./auditLogSafe.js");
await logAuditEventSafe({
action: AUDIT_ACTIONS.LOGIN,
userId: user.id,
resourceType: RESOURCE_TYPES.SESSION,
details: {
email: user.email,
role: user.role,
},
});
} catch (auditError) {
console.error("Failed to log audit event:", auditError);
}
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
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
token.userId = user.id;
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.userId;
session.user.role = token.role;
}
return session;
},
},
pages: {
signIn: "/auth/signin",
signOut: "/auth/signout",
error: "/auth/error",
},
debug: process.env.NODE_ENV === "development",
});