fix: code compliance audit and fix across all services
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped

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:
SpecialX
2026-07-09 17:28:27 +08:00
parent b53a486c6e
commit 0a71b02e04
93 changed files with 5775 additions and 608 deletions

View File

@@ -1,8 +1,15 @@
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { IamModule } from "./iam/iam.module.js";
import { HealthModule } from "./shared/health/health.module.js";
import { PermissionGuard } from "./middleware/permission.guard.js";
import { LifecycleService } from "./shared/lifecycle/lifecycle.service.js";
@Module({
imports: [IamModule, HealthModule],
providers: [
{ provide: APP_GUARD, useClass: PermissionGuard },
LifecycleService,
],
})
export class AppModule {}

View File

@@ -1,10 +1,11 @@
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import { env } from './env.js';
import { drizzle } from "drizzle-orm/mysql2";
import type { MySql2Database } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { env } from "./env.js";
let pool: mysql.Pool | null = null;
export function getDb() {
export function getDb(): MySql2Database {
if (!pool) {
pool = mysql.createPool({
uri: env.DATABASE_URL,

View File

@@ -1,37 +1,53 @@
import { Body, Controller, Get, Post, Req } from "@nestjs/common";
import type { Request } from "express";
import { IamService } from "./iam.service.js";
import type { TokenPair, UserInfo } from "./iam.service.js";
import { registerSchema, loginSchema, refreshTokenSchema } from "./iam.dto.js";
import { UnauthorizedError } from "../shared/errors/application-error.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
@Controller("iam")
export class IamController {
constructor(private readonly service: IamService) {}
// 公开端点:注册,不设权限校验
@Post("register")
async register(@Body() body: unknown) {
async register(
@Body() body: unknown,
): Promise<{ success: true; data: { user: UserInfo; tokens: TokenPair } }> {
const dto = registerSchema.parse(body);
const result = await this.service.register(dto);
return { success: true as const, data: result };
}
// 公开端点:登录,不设权限校验
@Post("login")
async login(@Body() body: unknown) {
async login(
@Body() body: unknown,
): Promise<{ success: true; data: { user: UserInfo; tokens: TokenPair } }> {
const dto = loginSchema.parse(body);
const result = await this.service.login(dto);
return { success: true as const, data: result };
}
// 公开端点:刷新令牌,不设权限校验
@Post("refresh")
async refresh(@Body() body: unknown) {
async refresh(
@Body() body: unknown,
): Promise<{ success: true; data: TokenPair }> {
const dto = refreshTokenSchema.parse(body);
const tokens = await this.service.refresh(dto.refreshToken);
return { success: true as const, data: tokens };
}
@Get("me")
async me(@Req() req: Request) {
const userId = req.headers["x-user-id"] as string;
@RequirePermission(Permissions.IAM_USER_READ)
async me(@Req() req: Request): Promise<{ success: true; data: UserInfo }> {
const userIdHeader = req.headers["x-user-id"];
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}

View File

@@ -10,6 +10,7 @@ import {
roleViewports,
} from "./iam.schema.js";
import type { User, Role, Permission, RoleViewport } from "./iam.schema.js";
import { DatabaseError } from "../shared/errors/application-error.js";
export class IamRepository {
async createUser(data: {
@@ -22,7 +23,7 @@ export class IamRepository {
await db.insert(users).values(data);
const [result] = await db.select().from(users).where(eq(users.id, data.id));
if (!result) {
throw new Error("Failed to create user");
throw new DatabaseError("Failed to create user");
}
return result;
}

View File

@@ -92,7 +92,11 @@ export class IamService {
async refresh(refreshToken: string): Promise<TokenPair> {
let payload: jwt.JwtPayload;
try {
payload = jwt.verify(refreshToken, env.JWT_SECRET) as jwt.JwtPayload;
const decoded = jwt.verify(refreshToken, env.JWT_SECRET);
if (typeof decoded === "string") {
throw new UnauthorizedError("Invalid refresh token");
}
payload = decoded;
} catch {
throw new UnauthorizedError("Invalid refresh token");
}
@@ -101,9 +105,14 @@ export class IamService {
throw new UnauthorizedError("Invalid token type");
}
const user = await this.repository.findUserById(payload.sub as string);
const sub = typeof payload.sub === "string" ? payload.sub : undefined;
if (!sub) {
throw new UnauthorizedError("Invalid token subject");
}
const user = await this.repository.findUserById(sub);
if (!user) {
throw new NotFoundError("User", payload.sub as string);
throw new NotFoundError("User", sub);
}
return this.issueTokens(user).then((r) => r.tokens);

View File

@@ -1,7 +1,13 @@
import { Controller, Get, Req } from "@nestjs/common";
import type { Request } from "express";
import { IamService } from "./iam.service.js";
import type { ViewportItem } from "./iam.service.js";
import type { Role, Permission } from "./iam.schema.js";
import { UnauthorizedError } from "../shared/errors/application-error.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
// RBAC 管理端点:角色/权限/视口查询
@Controller("iam")
@@ -10,8 +16,12 @@ export class RbacController {
// 获取当前用户的视口配置L1 导航)
@Get("viewports")
async viewports(@Req() req: Request) {
const userId = req.headers["x-user-id"] as string;
@RequirePermission(Permissions.IAM_USER_READ)
async viewports(
@Req() req: Request,
): Promise<{ success: true; data: ViewportItem[] }> {
const userIdHeader = req.headers["x-user-id"];
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}
@@ -21,8 +31,12 @@ export class RbacController {
// 获取当前用户的有效权限
@Get("permissions/effective")
async effectivePermissions(@Req() req: Request) {
const userId = req.headers["x-user-id"] as string;
@RequirePermission(Permissions.IAM_USER_READ)
async effectivePermissions(
@Req() req: Request,
): Promise<{ success: true; data: { permissions: string[] } }> {
const userIdHeader = req.headers["x-user-id"];
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}
@@ -32,14 +46,16 @@ export class RbacController {
// 列出所有角色(管理端用)
@Get("roles")
async roles() {
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
async roles(): Promise<{ success: true; data: Role[] }> {
const data = await this.service.getAllRoles();
return { success: true as const, data };
}
// 列出所有权限点(管理端用)
@Get("permissions")
async permissions() {
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
async permissions(): Promise<{ success: true; data: Permission[] }> {
const data = await this.service.getAllPermissions();
return { success: true as const, data };
}

View File

@@ -6,6 +6,7 @@ import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { env } from "./config/env.js";
import { logger } from "./shared/observability/logger.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
import type { Request, Response } from "express";
async function bootstrap(): Promise<void> {
initTracer();
@@ -19,7 +20,7 @@ async function bootstrap(): Promise<void> {
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
app.getHttpAdapter().get("/metrics", async (_req: Request, res: Response) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});

View File

@@ -1,5 +1,9 @@
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from "@nestjs/common";
import type { Request, Response, NextFunction } from "express";
export interface AuthenticatedRequest extends Request {
userId?: string;
@@ -10,15 +14,18 @@ export interface AuthenticatedRequest extends Request {
export class AuthMiddleware implements NestMiddleware {
use(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
// 从 Gateway 注入的头部读取用户信息
const userId = req.headers['x-user-id'] as string | undefined;
const rolesHeader = req.headers['x-user-roles'] 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) {
throw new UnauthorizedException('Missing x-user-id header');
throw new UnauthorizedException("Missing x-user-id header");
}
req.userId = userId;
req.userRoles = rolesHeader ? rolesHeader.split(',') : [];
req.userRoles = rolesHeader ? rolesHeader.split(",") : [];
next();
}
}

View File

@@ -1,23 +1,32 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import type { Reflector } from '@nestjs/core';
import { PermissionDeniedError } from '../shared/errors/application-error.js';
import type { AuthenticatedRequest } from './auth.middleware.js';
import {
Injectable,
CanActivate,
ExecutionContext,
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 type Permission =
| 'IAM_USER_CREATE'
| 'IAM_USER_READ'
| 'IAM_USER_UPDATE'
| 'IAM_USER_DELETE'
| 'IAM_ROLE_MANAGE';
| "IAM_USER_CREATE"
| "IAM_USER_READ"
| "IAM_USER_UPDATE"
| "IAM_USER_DELETE"
| "IAM_ROLE_MANAGE";
export const Permissions = {
IAM_USER_CREATE: 'IAM_USER_CREATE' as const,
IAM_USER_READ: 'IAM_USER_READ' as const,
IAM_USER_UPDATE: 'IAM_USER_UPDATE' as const,
IAM_USER_DELETE: 'IAM_USER_DELETE' as const,
IAM_ROLE_MANAGE: 'IAM_ROLE_MANAGE' as const,
IAM_USER_CREATE: "IAM_USER_CREATE" as const,
IAM_USER_READ: "IAM_USER_READ" as const,
IAM_USER_UPDATE: "IAM_USER_UPDATE" as const,
IAM_USER_DELETE: "IAM_USER_DELETE" as const,
IAM_ROLE_MANAGE: "IAM_ROLE_MANAGE" as const,
};
export const PERMISSIONS_KEY = "permissions";
export const RequirePermission = (...permissions: Permission[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
admin: [
Permissions.IAM_USER_CREATE,
@@ -31,26 +40,32 @@ const ROLE_PERMISSIONS: Record<string, Permission[]> = {
@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 roles = request.userRoles ?? [];
for (const role of roles) {
const perms = ROLE_PERMISSIONS[role];
if (perms && perms.includes(this.requiredPermission)) {
if (perms && requiredPermissions.some((p) => perms.includes(p))) {
return true;
}
}
throw new PermissionDeniedError(this.requiredPermission);
throw new PermissionDeniedError(requiredPermissions.join(", "));
}
}
// 工厂函数,用于装饰器
export function createPermissionGuardFactory(_reflector: Reflector) {
return {
create: (permission: Permission) => new PermissionGuard(permission),
};
}

View File

@@ -1,7 +1,13 @@
import { Catch, ExceptionFilter, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import { ZodError } from 'zod';
import { ApplicationError } from './application-error.js';
import {
Catch,
ExceptionFilter,
ArgumentsHost,
HttpException,
Logger,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { ZodError } from "zod";
import { ApplicationError } from "./application-error.js";
@Catch()
export class GlobalErrorFilter implements ExceptionFilter {
@@ -12,7 +18,9 @@ export class GlobalErrorFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const traceId = (request.headers['x-request-id'] as string | undefined) ?? 'unknown';
const traceIdHeader = request.headers["x-request-id"];
const traceId =
typeof traceIdHeader === "string" ? traceIdHeader : "unknown";
let statusCode = 500;
let body: Record<string, unknown>;
@@ -27,8 +35,8 @@ export class GlobalErrorFilter implements ExceptionFilter {
body = {
success: false,
error: {
code: 'IAM_VALIDATION_ERROR',
message: 'Validation failed',
code: "IAM_VALIDATION_ERROR",
message: "Validation failed",
details: exception.flatten(),
traceId,
},
@@ -40,7 +48,7 @@ export class GlobalErrorFilter implements ExceptionFilter {
body = {
success: false,
error: {
code: 'HTTP_ERROR',
code: "HTTP_ERROR",
message,
traceId,
},
@@ -53,8 +61,8 @@ export class GlobalErrorFilter implements ExceptionFilter {
body = {
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
traceId,
},
};
@@ -63,14 +71,17 @@ export class GlobalErrorFilter implements ExceptionFilter {
response.status(statusCode).json(body);
}
private extractHttpMessage(res: string | object, exception: HttpException): string {
if (typeof res === 'string') {
private extractHttpMessage(
res: string | object,
exception: HttpException,
): string {
if (typeof res === "string") {
return res;
}
if (res && typeof res === 'object' && 'message' in res) {
if (res && typeof res === "object" && "message" in res) {
// 从 HttpException 响应体收窄类型NestJS 约定包含 message 字段)
const msg = (res as { message: unknown }).message;
return typeof msg === 'string' ? msg : exception.message;
return typeof msg === "string" ? msg : exception.message;
}
return exception.message;
}