216 lines
5.7 KiB
TypeScript
216 lines
5.7 KiB
TypeScript
import { Request, Response, NextFunction } from "express";
|
|
import db from "../db";
|
|
import { createLogger, LogLevel } from "../lib/logger";
|
|
import { verifyJWT } from "./auth";
|
|
|
|
// Logger untuk middleware authentication
|
|
const authLogger = createLogger({
|
|
logDir: "./logs/auth",
|
|
filePrefix: "log-auth-middleware",
|
|
minLevel: LogLevel.INFO,
|
|
});
|
|
|
|
export interface AuthenticatedRequest extends Request {
|
|
user?: {
|
|
user_id: string;
|
|
username: string;
|
|
session_token: string;
|
|
};
|
|
}
|
|
|
|
// Middleware untuk autentikasi akses log
|
|
export async function authenticateLogAccess(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
) {
|
|
try {
|
|
const authHeader = req.headers.authorization;
|
|
const tokenFromQuery = req.query.token as string;
|
|
|
|
// Get token from header or query parameter
|
|
let token: string | null = null;
|
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
} else if (tokenFromQuery) {
|
|
token = tokenFromQuery;
|
|
}
|
|
|
|
if (!token) {
|
|
authLogger.warning("Log access attempt without token", {
|
|
ip: req.ip,
|
|
path: req.path,
|
|
userAgent: req.get("User-Agent"),
|
|
});
|
|
return res.status(401).json({
|
|
error: "TOKEN_REQUIRED",
|
|
message: "Authorization token is required",
|
|
});
|
|
}
|
|
|
|
// Verify JWT token
|
|
const decoded = verifyJWT(token);
|
|
if (!decoded || decoded.type !== "LOG_ACCESS") {
|
|
authLogger.warning("Invalid or expired log access JWT token", {
|
|
token: token.substring(0, 10) + "...",
|
|
ip: req.ip,
|
|
path: req.path,
|
|
});
|
|
return res.status(401).json({
|
|
error: "INVALID_TOKEN",
|
|
message: "Invalid or expired JWT token",
|
|
});
|
|
}
|
|
|
|
// Verify user still exists and is active
|
|
const user = await db.users.findFirst({
|
|
where: {
|
|
user_id: decoded.user_id,
|
|
is_active: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
authLogger.warning("JWT token for inactive/deleted user", {
|
|
userId: decoded.user_id,
|
|
ip: req.ip,
|
|
path: req.path,
|
|
});
|
|
return res.status(401).json({
|
|
error: "USER_INACTIVE",
|
|
message: "User account is inactive",
|
|
});
|
|
}
|
|
|
|
// Attach user info to request
|
|
req.user = {
|
|
user_id: decoded.user_id,
|
|
username: decoded.username,
|
|
session_token: token,
|
|
};
|
|
|
|
// authLogger.info("Log access authenticated", {
|
|
// userId: req.user.user_id,
|
|
// username: req.user.username,
|
|
// ip: req.ip,
|
|
// path: req.path,
|
|
// });
|
|
|
|
next();
|
|
} catch (err) {
|
|
authLogger.error("Log authentication error", {
|
|
error: err instanceof Error ? err.message : String(err),
|
|
ip: req.ip,
|
|
path: req.path,
|
|
});
|
|
return res.status(500).json({
|
|
error: "INTERNAL_ERROR",
|
|
message: "Authentication failed",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Middleware untuk rate limiting pada akses log
|
|
export function rateLimitLogAccess() {
|
|
const requestCounts = new Map<string, { count: number; resetTime: number }>();
|
|
const RATE_LIMIT = 100; // requests per window
|
|
const WINDOW_SIZE = 15 * 60 * 1000; // 15 minutes
|
|
|
|
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
const key = `${req.ip}:${req.user?.user_id || "anonymous"}`;
|
|
const now = Date.now();
|
|
|
|
let userRequests = requestCounts.get(key);
|
|
|
|
if (!userRequests || now > userRequests.resetTime) {
|
|
userRequests = { count: 1, resetTime: now + WINDOW_SIZE };
|
|
} else {
|
|
userRequests.count++;
|
|
}
|
|
|
|
requestCounts.set(key, userRequests);
|
|
|
|
if (userRequests.count > RATE_LIMIT) {
|
|
authLogger.warning("Rate limit exceeded for log access", {
|
|
userId: req.user?.user_id,
|
|
ip: req.ip,
|
|
requestCount: userRequests.count,
|
|
});
|
|
return res.status(429).json({
|
|
error: "RATE_LIMIT_EXCEEDED",
|
|
message: "Too many requests. Please try again later.",
|
|
resetTime: new Date(userRequests.resetTime).toISOString(),
|
|
});
|
|
}
|
|
|
|
// Cleanup old entries periodically
|
|
if (Math.random() < 0.01) {
|
|
// 1% chance
|
|
for (const [k, v] of requestCounts.entries()) {
|
|
if (now > v.resetTime) {
|
|
requestCounts.delete(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
// Optional: Middleware untuk audit trail
|
|
export function auditLogAccess() {
|
|
return async (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
const startTime = Date.now();
|
|
|
|
// Capture original res.json to log response
|
|
const originalJson = res.json;
|
|
let responseData: any;
|
|
|
|
res.json = function (data: any) {
|
|
responseData = data;
|
|
return originalJson.call(this, data);
|
|
};
|
|
|
|
res.on("finish", async () => {
|
|
const duration = Date.now() - startTime;
|
|
const auditData = {
|
|
userId: req.user?.user_id,
|
|
username: req.user?.username,
|
|
ip: req.ip,
|
|
method: req.method,
|
|
path: req.path,
|
|
query: req.query,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
userAgent: req.get("User-Agent"),
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
try {
|
|
// Log to database audit table if exists, or just log to file
|
|
await db.activity_logs.create({
|
|
data: {
|
|
action: "LOG_ACCESS",
|
|
status: res.statusCode < 400 ? "SUCCESS" : "ERROR",
|
|
message: `${req.method} ${req.path}`,
|
|
extra_json: JSON.stringify(auditData),
|
|
},
|
|
});
|
|
|
|
// authLogger.info("Log access audit", auditData);
|
|
} catch (err) {
|
|
authLogger.error("Failed to create audit log", {
|
|
error: err instanceof Error ? err.message : String(err),
|
|
auditData,
|
|
});
|
|
}
|
|
});
|
|
|
|
next();
|
|
};
|
|
}
|