first commit
This commit is contained in:
319
middleware/auth.js
Normal file
319
middleware/auth.js
Normal file
@@ -0,0 +1,319 @@
|
||||
import { verifyAccessToken } from "../utils/jwt.js";
|
||||
import User from "../models/User.js";
|
||||
import config from "../config/index.js";
|
||||
|
||||
/**
|
||||
* Middleware to verify JWT access token from cookies or Authorization header
|
||||
*/
|
||||
export const authenticate = async (request, reply) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
// DEBUG: Log incoming request details (only in development)
|
||||
if (config.isDevelopment) {
|
||||
console.log("\n=== AUTH MIDDLEWARE DEBUG ===");
|
||||
console.log("URL:", request.url);
|
||||
console.log("Method:", request.method);
|
||||
console.log("Cookies present:", Object.keys(request.cookies || {}));
|
||||
console.log("Has accessToken cookie:", !!request.cookies?.accessToken);
|
||||
console.log(
|
||||
"Authorization header:",
|
||||
request.headers.authorization ? "Present" : "Missing"
|
||||
);
|
||||
console.log("Origin:", request.headers.origin);
|
||||
console.log("Referer:", request.headers.referer);
|
||||
}
|
||||
|
||||
// Try to get token from Authorization header
|
||||
const authHeader = request.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
token = authHeader.substring(7);
|
||||
if (config.isDevelopment) {
|
||||
console.log("✓ Token found in Authorization header");
|
||||
}
|
||||
}
|
||||
|
||||
// If not in header, try cookies
|
||||
if (!token && request.cookies && request.cookies.accessToken) {
|
||||
token = request.cookies.accessToken;
|
||||
if (config.isDevelopment) {
|
||||
console.log("✓ Token found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
if (config.isDevelopment) {
|
||||
console.log("✗ No token found in cookies or headers");
|
||||
console.log("=== END AUTH DEBUG ===\n");
|
||||
}
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "No access token provided",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decoded = verifyAccessToken(token);
|
||||
|
||||
if (config.isDevelopment) {
|
||||
console.log("✓ Token verified, userId:", decoded.userId);
|
||||
}
|
||||
|
||||
// Fetch user from database
|
||||
const user = await User.findById(decoded.userId).select(
|
||||
"-twoFactor.secret"
|
||||
);
|
||||
|
||||
// Store token on request for session tracking
|
||||
request.token = token;
|
||||
|
||||
// Find the session associated with this token
|
||||
try {
|
||||
const Session = (await import("../models/Session.js")).default;
|
||||
const session = await Session.findOne({
|
||||
token: token,
|
||||
userId: decoded.userId,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (session) {
|
||||
request.sessionId = session._id;
|
||||
if (config.isDevelopment) {
|
||||
console.log("✓ Session found:", session._id);
|
||||
}
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.error("Error fetching session:", sessionError);
|
||||
// Don't fail auth if session lookup fails
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (config.isDevelopment) {
|
||||
console.log("✗ User not found in database");
|
||||
console.log("=== END AUTH DEBUG ===\n");
|
||||
}
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.isDevelopment) {
|
||||
console.log("✓ User authenticated:", user.username);
|
||||
console.log("=== END AUTH DEBUG ===\n");
|
||||
}
|
||||
|
||||
// Check if user is banned
|
||||
if (user.ban && user.ban.banned) {
|
||||
if (user.ban.expires && new Date(user.ban.expires) > new Date()) {
|
||||
return reply.status(403).send({
|
||||
error: "Forbidden",
|
||||
message: "Your account is banned",
|
||||
reason: user.ban.reason,
|
||||
expires: user.ban.expires,
|
||||
});
|
||||
} else if (!user.ban.expires) {
|
||||
return reply.status(403).send({
|
||||
error: "Forbidden",
|
||||
message: "Your account is permanently banned",
|
||||
reason: user.ban.reason,
|
||||
});
|
||||
} else {
|
||||
// Ban expired, clear it
|
||||
user.ban.banned = false;
|
||||
user.ban.reason = null;
|
||||
user.ban.expires = null;
|
||||
await user.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
request.user = user;
|
||||
} catch (error) {
|
||||
if (config.isDevelopment) {
|
||||
console.log("✗ Authentication error:", error.message);
|
||||
console.log("=== END AUTH DEBUG ===\n");
|
||||
}
|
||||
|
||||
if (error.message.includes("expired")) {
|
||||
return reply.status(401).send({
|
||||
error: "TokenExpired",
|
||||
message: "Access token has expired",
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "Invalid access token",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional authentication - doesn't fail if no token provided
|
||||
*/
|
||||
export const optionalAuthenticate = async (request, reply) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
// Try to get token from Authorization header
|
||||
const authHeader = request.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
token = authHeader.substring(7);
|
||||
}
|
||||
|
||||
// If not in header, try cookies
|
||||
if (!token && request.cookies && request.cookies.accessToken) {
|
||||
token = request.cookies.accessToken;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
request.user = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decoded = verifyAccessToken(token);
|
||||
|
||||
// Fetch user from database
|
||||
const user = await User.findById(decoded.userId).select(
|
||||
"-twoFactor.secret"
|
||||
);
|
||||
|
||||
if (user) {
|
||||
request.user = user;
|
||||
} else {
|
||||
request.user = null;
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't fail on optional auth
|
||||
request.user = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to check if user has required staff level
|
||||
* @param {number} requiredLevel - Minimum staff level required
|
||||
*/
|
||||
export const requireStaffLevel = (requiredLevel) => {
|
||||
return async (request, reply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "Authentication required",
|
||||
});
|
||||
}
|
||||
|
||||
if (request.user.staffLevel < requiredLevel) {
|
||||
return reply.status(403).send({
|
||||
error: "Forbidden",
|
||||
message: "Insufficient permissions",
|
||||
required: requiredLevel,
|
||||
current: request.user.staffLevel,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to check if user has verified email
|
||||
*/
|
||||
export const requireVerifiedEmail = async (request, reply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "Authentication required",
|
||||
});
|
||||
}
|
||||
|
||||
if (!request.user.email || !request.user.email.verified) {
|
||||
return reply.status(403).send({
|
||||
error: "Forbidden",
|
||||
message: "Email verification required",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to check if user has 2FA enabled when required
|
||||
*/
|
||||
export const require2FA = async (request, reply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "Authentication required",
|
||||
});
|
||||
}
|
||||
|
||||
if (!request.user.twoFactor || !request.user.twoFactor.enabled) {
|
||||
return reply.status(403).send({
|
||||
error: "Forbidden",
|
||||
message: "Two-factor authentication required",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to verify refresh token
|
||||
*/
|
||||
export const verifyRefreshTokenMiddleware = async (request, reply) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
// Try to get refresh token from cookies
|
||||
if (request.cookies && request.cookies.refreshToken) {
|
||||
token = request.cookies.refreshToken;
|
||||
}
|
||||
|
||||
// Try to get from body
|
||||
if (!token && request.body && request.body.refreshToken) {
|
||||
token = request.body.refreshToken;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "No refresh token provided",
|
||||
});
|
||||
}
|
||||
|
||||
// Import verifyRefreshToken here to avoid circular dependency
|
||||
const { verifyRefreshToken } = await import("../utils/jwt.js");
|
||||
const decoded = verifyRefreshToken(token);
|
||||
|
||||
// Fetch user from database
|
||||
const user = await User.findById(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Attach user and token to request
|
||||
request.user = user;
|
||||
request.refreshToken = token;
|
||||
} catch (error) {
|
||||
if (error.message.includes("expired")) {
|
||||
return reply.status(401).send({
|
||||
error: "TokenExpired",
|
||||
message: "Refresh token has expired",
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized",
|
||||
message: "Invalid refresh token",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
authenticate,
|
||||
optionalAuthenticate,
|
||||
requireStaffLevel,
|
||||
requireVerifiedEmail,
|
||||
require2FA,
|
||||
verifyRefreshTokenMiddleware,
|
||||
};
|
||||
Reference in New Issue
Block a user