fix: code compliance audit and fix across all services
NestJS (6 services): implement @RequirePermission decorator with SetMetadata+Reflector, register APP_GUARD globally, fix as assertions to type guards, add explicit return types, fix import type for express, fix /metrics implicit any, replace native Error with ApplicationError, remove typeorm remnants, register LifecycleService. teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward real userId to downstream, log downstream failures, migrate health controller to shared/health. Go (2 services): interface to any, doc comments, CORS dev whitelist, JWT secret fail-fast, push-gateway internal API auth, metrics and readyz endpoints, remove dead code. Python (2 services): lifespan return type, dev_mode to bool, data-ana APIRouter, ai POST body model, ClickHouse async wrapping.
This commit is contained in:
@@ -2,39 +2,29 @@ import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
id: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
} from "@nestjs/common";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: AuthenticatedUser;
|
||||
userId?: string;
|
||||
userRoles?: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthMiddleware implements NestMiddleware {
|
||||
use(req: AuthenticatedRequest, _res: Response, next: NextFunction): void {
|
||||
const userId = req.headers['x-user-id'] as string | undefined;
|
||||
const role = req.headers['x-user-role'] as string | undefined;
|
||||
const permissionsHeader = req.headers['x-user-permissions'] as
|
||||
| string
|
||||
| undefined;
|
||||
const userIdHeader = req.headers["x-user-id"];
|
||||
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
|
||||
const rolesHeaderRaw = req.headers["x-user-roles"];
|
||||
const rolesHeader =
|
||||
typeof rolesHeaderRaw === "string" ? rolesHeaderRaw : undefined;
|
||||
|
||||
if (!userId || !role) {
|
||||
throw new UnauthorizedException(
|
||||
'Missing authentication headers (x-user-id, x-user-role)',
|
||||
);
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException("Missing x-user-id header");
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: userId,
|
||||
role,
|
||||
permissions: permissionsHeader ? permissionsHeader.split(',') : [],
|
||||
};
|
||||
req.userId = userId;
|
||||
req.userRoles = rolesHeader ? rolesHeader.split(",") : [];
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,92 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import type { AuthenticatedRequest } from './auth.middleware.js';
|
||||
SetMetadata,
|
||||
} from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { PermissionDeniedError } from "../shared/errors/application-error.js";
|
||||
import type { AuthenticatedRequest } from "./auth.middleware.js";
|
||||
|
||||
export const Permissions = {
|
||||
EXAM_CREATE: 'exam:create',
|
||||
EXAM_READ: 'exam:read',
|
||||
EXAM_UPDATE: 'exam:update',
|
||||
EXAM_DELETE: 'exam:delete',
|
||||
HOMEWORK_CREATE: 'homework:create',
|
||||
HOMEWORK_READ: 'homework:read',
|
||||
HOMEWORK_UPDATE: 'homework:update',
|
||||
HOMEWORK_DELETE: 'homework:delete',
|
||||
HOMEWORK_GRADE: 'homework:grade',
|
||||
HOMEWORK_SUBMIT: 'homework:submit',
|
||||
GRADE_CREATE: 'grade:create',
|
||||
GRADE_READ: 'grade:read',
|
||||
GRADE_UPDATE: 'grade:update',
|
||||
GRADE_DELETE: 'grade:delete',
|
||||
CLASS_MANAGE: 'class:manage',
|
||||
CLASS_READ: 'class:read',
|
||||
CLASS_TRANSFER: 'class:transfer',
|
||||
EXAM_CREATE: "CORE_EDU_EXAM_CREATE" as const,
|
||||
EXAM_READ: "CORE_EDU_EXAM_READ" as const,
|
||||
EXAM_UPDATE: "CORE_EDU_EXAM_UPDATE" as const,
|
||||
EXAM_DELETE: "CORE_EDU_EXAM_DELETE" as const,
|
||||
HOMEWORK_CREATE: "CORE_EDU_HOMEWORK_CREATE" as const,
|
||||
HOMEWORK_READ: "CORE_EDU_HOMEWORK_READ" as const,
|
||||
HOMEWORK_UPDATE: "CORE_EDU_HOMEWORK_UPDATE" as const,
|
||||
HOMEWORK_SUBMIT: "CORE_EDU_HOMEWORK_SUBMIT" as const,
|
||||
GRADE_CREATE: "CORE_EDU_GRADE_CREATE" as const,
|
||||
GRADE_READ: "CORE_EDU_GRADE_READ" as const,
|
||||
} as const;
|
||||
|
||||
export type Permission = (typeof Permissions)[keyof typeof Permissions];
|
||||
|
||||
export const PERMISSIONS_KEY = "permissions";
|
||||
export const RequirePermission = (...permissions: Permission[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
|
||||
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
|
||||
admin: [
|
||||
Permissions.EXAM_CREATE,
|
||||
Permissions.EXAM_READ,
|
||||
Permissions.EXAM_UPDATE,
|
||||
Permissions.EXAM_DELETE,
|
||||
Permissions.HOMEWORK_CREATE,
|
||||
Permissions.HOMEWORK_READ,
|
||||
Permissions.HOMEWORK_UPDATE,
|
||||
Permissions.HOMEWORK_SUBMIT,
|
||||
Permissions.GRADE_CREATE,
|
||||
Permissions.GRADE_READ,
|
||||
],
|
||||
teacher: [
|
||||
Permissions.EXAM_CREATE,
|
||||
Permissions.EXAM_READ,
|
||||
Permissions.EXAM_UPDATE,
|
||||
Permissions.HOMEWORK_CREATE,
|
||||
Permissions.HOMEWORK_READ,
|
||||
Permissions.HOMEWORK_UPDATE,
|
||||
Permissions.HOMEWORK_SUBMIT,
|
||||
Permissions.GRADE_CREATE,
|
||||
Permissions.GRADE_READ,
|
||||
],
|
||||
student: [
|
||||
Permissions.EXAM_READ,
|
||||
Permissions.HOMEWORK_READ,
|
||||
Permissions.HOMEWORK_SUBMIT,
|
||||
Permissions.GRADE_READ,
|
||||
],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
constructor(private readonly requiredPermission: Permission) {}
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const user = request.user;
|
||||
if (!user) {
|
||||
throw new ForbiddenException('User not authenticated');
|
||||
const roles = request.userRoles ?? [];
|
||||
|
||||
for (const role of roles) {
|
||||
const perms = ROLE_PERMISSIONS[role];
|
||||
if (perms && requiredPermissions.some((p) => perms.includes(p))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!user.permissions.includes(this.requiredPermission)) {
|
||||
throw new ForbiddenException(
|
||||
`Missing permission: ${this.requiredPermission}`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
|
||||
throw new PermissionDeniedError(requiredPermissions.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user