///
import { NextFunction, Request, Response } from "express";
import { authenticateUser, AuthJWTPayload } from "../../Controllers/admin_auth.controller";
import { authClient } from "../../lib/redis";
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
import prisma from "../../lib/prisma";
import AuthError from "../error/AuthError";
import { TeamleaderJWTPayload, _requireTeamleaderAuthentication } from "./teamleaderAuth";
require("express-async-errors");
const JWT_SECRET = process.env.JWT_SECRET;
export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization);
export const getBearerToken = (authorization: string) => authorization.slice(7);
const _requireAdminAuthentication =
(config: { optional?: Boolean; controlled?: Boolean } = { optional: false, controlled: false }) =>
async (req: Request, res: Response, next: NextFunction) => {
if (!JWT_SECRET) {
throw new Error("JWT_SECRET not set");
}
const { authorization } = req.headers;
if (!authorization) {
if (config.optional) {
return false;
}
return res.status(403).send({
type: "error",
payload: {
message: "The requeset did not include the Authorization header",
},
});
}
if (!verifyAuthorizationFormat(authorization)) {
return res.status(400).send({
type: "error",
payload: {
message: "Malformed Authorization header",
format: "Bearer ",
},
});
}
let token_payload_: string | JwtPayload;
try {
token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
} catch (e) {
if (e instanceof JsonWebTokenError) {
return res.status(403).json({
type: "error",
payload: {
message: "Token could not be verified; It might be expired",
},
});
}
throw e;
}
const token_payload = token_payload_ as AuthJWTPayload;
if (!token_payload.permission_level || !token_payload.pid || !token_payload.revision) {
if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string") {
throw new AuthError("Teamleader authentication is not supported for this operation!");
}
throw new AuthError("The token did not include the required information!");
}
const { pid, revision } = token_payload;
let db_revision = await authClient.get(pid);
if (db_revision === null) {
// Load the revision ID from the main DB and cache it in redis
const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } });
if (user) {
db_revision = user.revision.toISOString();
await authClient.set(pid, db_revision);
}
}
if (revision !== db_revision || !revision || !db_revision) {
return res.status(403).json({
type: "error",
payload: {
message: "Token could not be verified; It might be expired",
},
});
}
req.auth = {
isAuthenticated: true,
pid: token_payload.pid,
name: token_payload.name,
permission_level: token_payload.permission_level,
groups: token_payload.groups,
revision: token_payload.revision,
};
if (!config.controlled) {
next();
}
return true;
};
export const requireAuthentication = _requireAdminAuthentication({ optional: false, controlled: false });
type AuthType = "admin" | "teamleader";
interface AuthTypeConfig {
admin?: Boolean;
teamleader?: Boolean;
}
interface AuthConfiguration {
type: AuthType | AuthTypeConfig;
optional: Boolean;
}
function getAuthTypes(type: AuthType | AuthTypeConfig): AuthType[] {
if (typeof type === "string") {
return [type];
}
return Object.entries(type)
.filter(([_, value]) => value)
.map(([key, _]) => key as AuthType);
}
export const requireConfiguredAuthentication =
(config: AuthConfiguration = { optional: false, type: "admin" }) =>
async (req: Request, res: Response, next: NextFunction) => {
const types = getAuthTypes(config.type);
const optional = config.optional;
let adminFinished = false;
let teamleaderFinished = false;
if (types.includes("admin")) {
adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next));
if (adminFinished) {
return next();
}
}
if (types.includes("teamleader")) {
teamleaderFinished = Boolean(
_requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next)
);
if (teamleaderFinished) {
return next();
}
}
if (!config.optional) {
throw new AuthError("No sufficient authorization was provided for this operation");
}
next();
};
export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) {
if (auth?.permission_level === "ELEVATED") {
return;
}
if (!auth?.groups.includes(groupPid)) {
throw new AuthError("The provided authorization is not valid for the requested operation!");
}
}