159 lines
3.6 KiB
TypeScript
Executable File
159 lines
3.6 KiB
TypeScript
Executable File
import {
|
|
Injectable,
|
|
CanActivate,
|
|
ExecutionContext,
|
|
UnauthorizedException,
|
|
ForbiddenException,
|
|
} from "@nestjs/common";
|
|
import { Reflector } from "@nestjs/core";
|
|
import { AuthGuard } from "@nestjs/passport";
|
|
import { Request } from "express";
|
|
import {
|
|
IS_PUBLIC_KEY,
|
|
ROLES_KEY,
|
|
PERMISSIONS_KEY,
|
|
} from "../../../common/decorators";
|
|
import { normalizeRole } from "../../../common/constants/roles";
|
|
|
|
interface AuthenticatedUser {
|
|
id: string;
|
|
email: string;
|
|
roles: string[];
|
|
role?: string;
|
|
permissions: string[];
|
|
}
|
|
|
|
/**
|
|
* JWT Auth Guard - Validates JWT token
|
|
*/
|
|
@Injectable()
|
|
export class JwtAuthGuard extends AuthGuard("jwt") {
|
|
constructor(private reflector: Reflector) {
|
|
super();
|
|
}
|
|
|
|
canActivate(context: ExecutionContext) {
|
|
const request = context.switchToHttp().getRequest<Request>();
|
|
if (request?.method === "OPTIONS") {
|
|
return true;
|
|
}
|
|
|
|
// Check if route is public
|
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
|
|
if (isPublic) {
|
|
return true;
|
|
}
|
|
|
|
return super.canActivate(context);
|
|
}
|
|
|
|
handleRequest<TUser = AuthenticatedUser>(
|
|
err: Error | null,
|
|
user: TUser | false,
|
|
info: any,
|
|
): TUser {
|
|
if (err || !user) {
|
|
if (info?.name === "TokenExpiredError") {
|
|
throw new UnauthorizedException("TOKEN_EXPIRED");
|
|
}
|
|
throw err || new UnauthorizedException("AUTH_REQUIRED");
|
|
}
|
|
return user;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Roles Guard - Check if user has required roles
|
|
*/
|
|
@Injectable()
|
|
export class RolesGuard implements CanActivate {
|
|
constructor(private reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const req = context.switchToHttp().getRequest<Request>();
|
|
if (req?.method === "OPTIONS") {
|
|
return true;
|
|
}
|
|
|
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
|
ROLES_KEY,
|
|
[context.getHandler(), context.getClass()],
|
|
);
|
|
|
|
if (!requiredRoles || requiredRoles.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const user = req.user as AuthenticatedUser | undefined;
|
|
|
|
if (!user) {
|
|
return false;
|
|
}
|
|
|
|
const normalizedUserRoles = (
|
|
user.roles?.length ? user.roles : user.role ? [user.role] : []
|
|
).map((role) => normalizeRole(role));
|
|
|
|
const normalizedRequiredRoles = requiredRoles.map((role) =>
|
|
normalizeRole(role),
|
|
);
|
|
|
|
if (normalizedUserRoles.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const hasRole = normalizedRequiredRoles.some((role) =>
|
|
normalizedUserRoles.includes(role),
|
|
);
|
|
if (!hasRole) {
|
|
throw new ForbiddenException("PERMISSION_DENIED");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Permissions Guard - Check if user has required permissions
|
|
*/
|
|
@Injectable()
|
|
export class PermissionsGuard implements CanActivate {
|
|
constructor(private reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const req = context.switchToHttp().getRequest<Request>();
|
|
if (req?.method === "OPTIONS") {
|
|
return true;
|
|
}
|
|
|
|
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
|
PERMISSIONS_KEY,
|
|
[context.getHandler(), context.getClass()],
|
|
);
|
|
|
|
if (!requiredPermissions || requiredPermissions.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const user = req.user as AuthenticatedUser | undefined;
|
|
|
|
if (!user || !user.permissions) {
|
|
return false;
|
|
}
|
|
|
|
const hasPermission = requiredPermissions.every((permission) =>
|
|
user.permissions.includes(permission),
|
|
);
|
|
|
|
if (!hasPermission) {
|
|
throw new ForbiddenException("PERMISSION_DENIED");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|