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:
19
services/teacher-bff/Dockerfile
Normal file
19
services/teacher-bff/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY tsconfig.json nest-cli.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
# Runtime stage
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3003
|
||||
CMD ["node", "dist/main.js"]
|
||||
8
services/teacher-bff/nest-cli.json
Normal file
8
services/teacher-bff/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
31
services/teacher-bff/package.json
Normal file
31
services/teacher-bff/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@edu/teacher-bff",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"start": "node dist/main.js",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.0",
|
||||
"@nestjs/core": "^10.4.0",
|
||||
"@nestjs/platform-express": "^10.4.0",
|
||||
"pino": "^9.4.0",
|
||||
"prom-client": "^15.1.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"eslint": "^9.10.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
7
services/teacher-bff/src/app.module.ts
Normal file
7
services/teacher-bff/src/app.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeacherModule } from './teacher/teacher.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [TeacherModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
25
services/teacher-bff/src/config/env.ts
Normal file
25
services/teacher-bff/src/config/env.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.string().default('3003'),
|
||||
IamServiceUrl: z.string().url().default('http://localhost:3002'),
|
||||
ClassesServiceUrl: z.string().url().default('http://localhost:3001'),
|
||||
LOG_LEVEL: z.string().default('info'),
|
||||
NODE_ENV: z.string().default('development'),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
export function loadEnv(): Env {
|
||||
const result = envSchema.safeParse({
|
||||
...process.env,
|
||||
IamServiceUrl: process.env.IAM_SERVICE_URL || 'http://localhost:3002',
|
||||
ClassesServiceUrl: process.env.CLASSES_SERVICE_URL || 'http://localhost:3001',
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error('Invalid env: ' + JSON.stringify(result.error.flatten()));
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export const env = loadEnv();
|
||||
24
services/teacher-bff/src/main.ts
Normal file
24
services/teacher-bff/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module.js';
|
||||
import { env } from './config/env.js';
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['log', 'error', 'warn'],
|
||||
});
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
||||
await app.listen(env.PORT);
|
||||
console.log(`Teacher BFF started on port ${env.PORT}`);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap().catch((err: unknown) => {
|
||||
console.error('Failed to start Teacher BFF', err);
|
||||
process.exit(1);
|
||||
});
|
||||
19
services/teacher-bff/src/teacher/teacher.controller.ts
Normal file
19
services/teacher-bff/src/teacher/teacher.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, Req } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { TeacherService } from './teacher.service.js';
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
@Controller('teacher')
|
||||
export class TeacherController {
|
||||
constructor(private readonly service: TeacherService) {}
|
||||
|
||||
@Get('dashboard')
|
||||
async dashboard(@Req() req: Request) {
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const data = await this.service.getDashboard(authReq.userId as string);
|
||||
return { success: true, data };
|
||||
}
|
||||
}
|
||||
9
services/teacher-bff/src/teacher/teacher.module.ts
Normal file
9
services/teacher-bff/src/teacher/teacher.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeacherController } from './teacher.controller.js';
|
||||
import { TeacherService } from './teacher.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [TeacherController],
|
||||
providers: [TeacherService],
|
||||
})
|
||||
export class TeacherModule {}
|
||||
22
services/teacher-bff/src/teacher/teacher.service.ts
Normal file
22
services/teacher-bff/src/teacher/teacher.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
@Injectable()
|
||||
export class TeacherService {
|
||||
// 聚合 IAM + classes 服务的数据
|
||||
async getDashboard(userId: string): Promise<unknown> {
|
||||
const [iamRes, classesRes] = await Promise.allSettled([
|
||||
fetch(`${env.IamServiceUrl}/iam/me`, {
|
||||
headers: { 'x-user-id': userId },
|
||||
}),
|
||||
fetch(`${env.ClassesServiceUrl}/classes`, {
|
||||
headers: { 'x-user-id': userId },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
user: iamRes.status === 'fulfilled' ? await iamRes.value.json() : null,
|
||||
classes: classesRes.status === 'fulfilled' ? await classesRes.value.json() : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
services/teacher-bff/tsconfig.json
Normal file
15
services/teacher-bff/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
Reference in New Issue
Block a user