feat(p2): identity layer with IAM service and Teacher BFF

P2 阶段交付物:
- services/iam: 完整身份认证服务(users/roles/permissions/refresh_tokens 6 表 schema)
  - register/login/refresh/getUserInfo 4 个核心 API
  - bcrypt 密码哈希 + JWT 双 Token(access + refresh)
  - 复用 classes 黄金模板(errors/observability/middleware 三件套)
- services/teacher-bff: 教师聚合 BFF
  - Promise.allSettled 并行聚合 IAM + classes 数据
  - /teacher/dashboard 单一聚合端点
- packages/shared-proto/proto/iam.proto: IamService 契约(Register/Login/RefreshToken/GetUserInfo)
- api-gateway: 新增 IamServiceURL/TeacherBffURL 配置 + /iam/* + /teacher/* 路由
This commit is contained in:
SpecialX
2026-07-08 01:37:29 +08:00
parent 2ba4250165
commit 524204d30a
35 changed files with 1108 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
export interface AuthenticatedRequest extends Request {
userId?: string;
userRoles?: string[];
}
@Injectable()
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;
if (!userId) {
throw new UnauthorizedException('Missing x-user-id header');
}
req.userId = userId;
req.userRoles = rolesHeader ? rolesHeader.split(',') : [];
next();
}
}

View File

@@ -0,0 +1,56 @@
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';
export type Permission =
| '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,
};
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
admin: [
Permissions.IAM_USER_CREATE,
Permissions.IAM_USER_READ,
Permissions.IAM_USER_UPDATE,
Permissions.IAM_USER_DELETE,
Permissions.IAM_ROLE_MANAGE,
],
teacher: [Permissions.IAM_USER_READ],
};
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly requiredPermission: Permission) {}
canActivate(context: ExecutionContext): boolean {
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)) {
return true;
}
}
throw new PermissionDeniedError(this.requiredPermission);
}
}
// 工厂函数,用于装饰器
export function createPermissionGuardFactory(_reflector: Reflector) {
return {
create: (permission: Permission) => new PermissionGuard(permission),
};
}