All checks were successful
Build Frontend / Build Frontend (push) Successful in 10s
336 lines
9.0 KiB
JavaScript
336 lines
9.0 KiB
JavaScript
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) {
|
|
// Check if ban has expired
|
|
if (user.ban.expires && new Date(user.ban.expires) <= new Date()) {
|
|
// Ban expired, clear it
|
|
user.ban.banned = false;
|
|
user.ban.reason = null;
|
|
user.ban.expires = null;
|
|
await user.save();
|
|
} else {
|
|
// User is currently banned
|
|
// Allow access to /api/auth/me so frontend can get ban info and redirect
|
|
const url = request.url || "";
|
|
const routeUrl = request.routeOptions?.url || "";
|
|
const isAuthMeEndpoint =
|
|
url.includes("/auth/me") ||
|
|
routeUrl === "/me" ||
|
|
routeUrl.endsWith("/me");
|
|
|
|
if (!isAuthMeEndpoint) {
|
|
// Block access to all other endpoints
|
|
if (user.ban.expires) {
|
|
return reply.status(403).send({
|
|
error: "Forbidden",
|
|
message: "Your account is banned",
|
|
reason: user.ban.reason,
|
|
expires: user.ban.expires,
|
|
});
|
|
} else {
|
|
return reply.status(403).send({
|
|
error: "Forbidden",
|
|
message: "Your account is permanently banned",
|
|
reason: user.ban.reason,
|
|
});
|
|
}
|
|
}
|
|
// If it's /api/auth/me, continue and attach user with ban info
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
};
|