From d346a9f2f49a37635184b89f238a941101e9466d Mon Sep 17 00:00:00 2001 From: dayjoy Date: Fri, 26 Sep 2025 15:50:37 +0800 Subject: [PATCH] feat(server): add error handler & refactor controllers --- .../server/src/controllers/GroupController.ts | 68 ++++++++ .../server/src/controllers/TestController.ts | 3 +- .../server/src/controllers/UserController.ts | 26 ++++ .../src/controllers/UserGroupController.ts | 30 ---- packages/server/src/index.ts | 10 +- packages/server/src/middleware/auth.ts | 145 +++--------------- .../server/src/middleware/errorHandler.ts | 17 ++ packages/server/src/services/.gitkeep | 0 packages/server/src/types/api.ts | 18 +-- packages/server/src/types/express.ts | 9 ++ packages/server/src/types/user.ts | 11 ++ packages/server/tsconfig.json | 3 - packages/server/tsoa.json | 5 +- 13 files changed, 173 insertions(+), 172 deletions(-) create mode 100644 packages/server/src/controllers/GroupController.ts create mode 100644 packages/server/src/controllers/UserController.ts delete mode 100644 packages/server/src/controllers/UserGroupController.ts create mode 100644 packages/server/src/middleware/errorHandler.ts delete mode 100644 packages/server/src/services/.gitkeep create mode 100644 packages/server/src/types/express.ts create mode 100644 packages/server/src/types/user.ts diff --git a/packages/server/src/controllers/GroupController.ts b/packages/server/src/controllers/GroupController.ts new file mode 100644 index 0000000..1b577c9 --- /dev/null +++ b/packages/server/src/controllers/GroupController.ts @@ -0,0 +1,68 @@ +import {Controller, Get, Route, Response, Tags, Request, Security} from 'tsoa'; +import {ApiError, ApiResponse} from '../types/api'; +import type {Request as ExpressRequest} from 'express'; +import axios from "axios"; +import type {UserInfo} from "../types/user"; + +export const getGroupUsers = async (groupId: number, token: string): Promise => { + try { + const response = await axios.post>( + "https://egret.byteawake.com/api/group/members", + {groupId}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + timeout: 10000, // 10 second timeout + } + ); + + if (response.data.code !== 200) { + throw new Error(`Failed to get group users: ${response.data.message}`); + } + + return response.data.data ?? []; + } catch (error: any) { + if (error.response) { + // API returned error response + throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + // Request was sent but no response received + throw new Error("Failed to get user information: timeout or network error"); + } else { + // Other errors + throw new Error(`Failed to get user information: ${error.message}`); + } + } +}; + +@Route('api/group') +@Tags('Chat Group') +export class GroupController extends Controller { + /** + * 获取当前聊天群组的全部用户信息 + * @summary 获取当前聊天群组的全部用户信息 + * @description 获取当前聊天群组的全部用户信息 + */ + @Get('/users') + @Security("jwt") + @Response>(200, 'Success') + @Response>(400, 'Bad Request') + @Response>(401, 'Unauthorized') + public async getGroupUsers(@Request() req: ExpressRequest): Promise> { + const user = req.user; + const groupId = req.headers.groupid; + + if (!groupId) { + throw new ApiError(400, 'groupId is required in headers'); + } + + const users = await getGroupUsers(Number(groupId), user.token); + + return { + code: 200, + message: 'success', + data: users, + }; + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/TestController.ts b/packages/server/src/controllers/TestController.ts index 238334f..b200750 100644 --- a/packages/server/src/controllers/TestController.ts +++ b/packages/server/src/controllers/TestController.ts @@ -1,5 +1,5 @@ import { Controller, Get, Route, Response, Tags } from 'tsoa'; -import { ApiResponse } from '../types/api'; +import type { ApiResponse } from '../types/api'; @Route('api') @Tags('Test') @@ -13,6 +13,7 @@ export class TestController extends Controller { @Response(200, 'Success') public async getTest(): Promise { return { + code: 200, message: 'success', data: null, }; diff --git a/packages/server/src/controllers/UserController.ts b/packages/server/src/controllers/UserController.ts new file mode 100644 index 0000000..ece1a03 --- /dev/null +++ b/packages/server/src/controllers/UserController.ts @@ -0,0 +1,26 @@ +import {Controller, Get, Route, Response, Tags, Middlewares, Request, Security} from 'tsoa'; +import type { ApiResponse } from '../types/api'; +import type { Request as ExpressRequest } from 'express'; +import type {UserInfo} from "../types/user"; + + +@Route('api/user') +@Tags('User') +export class UserController extends Controller { + /** + * 获取当前用户的信息 + * @summary 获取当前用户的信息 + * @description 获取当前用户的信息 + */ + @Get('/info') + @Security('jwt') + @Response>(200, 'Success') + @Response(401, 'Unauthorized') + public async getUserGroupInfo(@Request() req: ExpressRequest): Promise> { + return { + code: 200, + message: 'success', + data: req.user + }; + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserGroupController.ts b/packages/server/src/controllers/UserGroupController.ts deleted file mode 100644 index 78e31fd..0000000 --- a/packages/server/src/controllers/UserGroupController.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Controller, Get, Route, Response, Tags, Middlewares, Request } from 'tsoa'; -import { ApiResponse, UserGroupInfo } from '../types/api'; -import { userInfoMiddleware, groupInfoMiddleware } from '../middleware/auth'; -import type { Request as ExpressRequest } from 'express'; - -@Route('api') -@Tags('User Group') -export class UserGroupController extends Controller { - /** - * 获取用户群组信息 - * @summary 获取用户群组信息 - * @description 获取当前用户的个人信息和所在群组的所有用户信息 - */ - @Get('/user-group/info') - @Middlewares([userInfoMiddleware, groupInfoMiddleware]) - @Response>(200, 'Success') - @Response(401, 'Unauthorized') - public async getUserGroupInfo(@Request() req: ExpressRequest): Promise> { - const userInfo = req.userInfo; - const groupInfo = req.groupInfo; - - return { - message: 'success', - data: { - user: userInfo || null, - group: groupInfo || null - } - }; - } -} \ No newline at end of file diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 3e413ae..7b38bc4 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -8,8 +8,9 @@ import path from "path"; dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); import "./database"; -import { sequelize } from "@/database/instance"; -import { RegisterTsoaRoutes } from "./middleware/tsoa.middleware"; +import { sequelize } from "./database/instance"; +import {errorHandler} from "./middleware/errorHandler"; +import { RegisterRoutes } from "./routes/routes"; // tsoa 生成的 const port = process.env.PORT || 3005; @@ -19,11 +20,12 @@ app.use(nocache()); app.use(cors()); app.use(express.json({ limit: "100mb" })); app.use(compression()); - app.use(express.static(path.resolve(__dirname, "client"))); // Register tsoa routes -RegisterTsoaRoutes(app); +RegisterRoutes(app); + +app.use(errorHandler); const host = "0.0.0.0"; diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts index d0adb5e..3e3f127 100644 --- a/packages/server/src/middleware/auth.ts +++ b/packages/server/src/middleware/auth.ts @@ -1,27 +1,25 @@ -import {Request, Response, NextFunction} from "express"; +import {Request} from "express"; import axios from "axios"; +import {ApiError, ApiResponse} from "../types/api"; +import type {UserInfo} from "../types/user"; -export type UserInfo = { - userId: number; - nickname: string; - avatarUrl: string; - gender: 'MALE' | 'FEMALE' | 'UNKNOWN'; - nimToken: string; // NetEase Cloud Communication token - nimAccountId: string; // NetEase Cloud Communication account ID - createdAt: string; - updatedAt: string; -} +export async function expressAuthentication( + request: Request, + securityName: string, + scopes?: string[] +): Promise { + if (securityName === "jwt") { + const authHeader = request.headers.authorization; + const token = extractTokenFromHeader(authHeader); -declare global { - namespace Express { - interface Request { - userInfo?: UserInfo | null; - groupInfo?: { - groupId: number; - users: UserInfo[]; - } | null; + if (!token) { + throw new ApiError(401, "Unauthorized"); } + + // 返回的对象会挂到 request.user 上 + return await getUserInfoByToken(token); } + throw new ApiError(401, "Unsupported security scheme"); } /** @@ -43,9 +41,9 @@ export const extractTokenFromHeader = (authHeader: string | undefined): string | /** * Get user information by token */ -export const getUserInfoByToken = async (token: string): Promise => { +export const getUserInfoByToken = async (token: string): Promise => { try { - const response = await axios.post( + const response = await axios.post>>( "https://egret.byteawake.com/api/user/info", {}, // 请求体数据,这里为空对象 { @@ -56,74 +54,11 @@ export const getUserInfoByToken = async (token: string): Promise => { } ); - return response.data; - } catch (error: any) { - if (error.response) { - // API returned error response - throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`); - } else if (error.request) { - // Request was sent but no response received - throw new Error("Failed to get user information: timeout or network error"); - } else { - // Other errors - throw new Error(`Failed to get user information: ${error.message}`); - } - } -}; - -/** - * User information middleware - * Automatically extract token from request header and get user information, store result in req.userInfo - */ -export const userInfoMiddleware = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { - try { - const authHeader = req.headers.authorization; - const token = extractTokenFromHeader(authHeader); - - if (!token) { - res.status(401).json({ - error: "No valid access token provided", - message: "Missing Bearer token in Authorization header", - }); - return; + if (response.data.code !== 200 || !response.data.data) { + throw new Error(`Failed to get user information: ${response.data.message}`); } - // Get user information - const userInfoRes = await getUserInfoByToken(token); - req.userInfo = userInfoRes.code === 200 ? userInfoRes.data : null; - - next(); - } catch (error: any) { - res.status(401).json({ - error: "User authentication failed", - message: error.message, - }); - } -}; - -//////// group - -/** - * Get group users - */ -export const getGroupUsers = async (groupId: number, token: string): Promise => { - try { - const response = await axios.post( - "https://egret.byteawake.com/api/group/members", - {groupId}, - { - headers: { - Authorization: `Bearer ${token}`, - }, - timeout: 10000, // 10 second timeout - } - ); - - return response.data; + return {...response.data.data, token}; } catch (error: any) { if (error.response) { // API returned error response @@ -139,40 +74,4 @@ export const getGroupUsers = async (groupId: number, token: string): Promise => { - try { - const authHeader = req.headers.authorization; - const token = extractTokenFromHeader(authHeader); - const groupId = req.headers.groupid; - - if (token && groupId) { - try { - const usersRes = await getGroupUsers(Number(groupId), token); - const users: UserInfo[] = usersRes.code === 200 ? usersRes.data : []; - req.groupInfo = { - groupId: Number(groupId), - users, - } - } catch (error) { - // If getting user information fails, do not block the request from continuing, but do not set userInfo - console.warn("Failed to get group user information:", error); - } - } - - next(); - } catch (error: any) { - res.status(400).json({ - error: "Get Group Users failed", - message: error.message, - }); - } -}; - diff --git a/packages/server/src/middleware/errorHandler.ts b/packages/server/src/middleware/errorHandler.ts new file mode 100644 index 0000000..8c0a95c --- /dev/null +++ b/packages/server/src/middleware/errorHandler.ts @@ -0,0 +1,17 @@ +import {Request, Response, NextFunction} from "express"; +import {ApiError} from "../types/api"; + +export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) { + console.error(err); + + if (err instanceof ApiError) { + return res.status(err.status).json({code: err.status, message: err.message, data: null}); + } + + // tsoa 内部生成的验证错误 + if (err.status && err.status >= 400) { + return res.status(err.status).json({ message: err.message }); + } + + res.status(500).json({ message: 'Internal Server Error' }); +} diff --git a/packages/server/src/services/.gitkeep b/packages/server/src/services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/server/src/types/api.ts b/packages/server/src/types/api.ts index cdde05d..e804588 100644 --- a/packages/server/src/types/api.ts +++ b/packages/server/src/types/api.ts @@ -1,14 +1,14 @@ -import { UserInfo } from '../middleware/auth'; - export interface ApiResponse { + code: number; message: string; data: T | null; } -export interface UserGroupInfo { - user: UserInfo | null; - group: { - groupId: number; - users: UserInfo[]; - } | null; -} +export class ApiError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + Object.setPrototypeOf(this, ApiError.prototype); + } +} \ No newline at end of file diff --git a/packages/server/src/types/express.ts b/packages/server/src/types/express.ts new file mode 100644 index 0000000..f3f8a4a --- /dev/null +++ b/packages/server/src/types/express.ts @@ -0,0 +1,9 @@ +import {UserInfo} from "./user"; + +declare global { + namespace Express { + interface Request { + user: UserInfo; + } + } +} \ No newline at end of file diff --git a/packages/server/src/types/user.ts b/packages/server/src/types/user.ts new file mode 100644 index 0000000..277aed8 --- /dev/null +++ b/packages/server/src/types/user.ts @@ -0,0 +1,11 @@ +export interface UserInfo { + userId: number; + nickname: string; + avatarUrl: string; + gender: 'MALE' | 'FEMALE' | 'UNKNOWN'; + nimToken: string; // NetEase Cloud Communication token + nimAccountId: string; // NetEase Cloud Communication account ID + createdAt: string; + updatedAt: string; + token: string; // Authentication token +} \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index e08d847..59cb735 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -6,9 +6,6 @@ "module": "node16", "moduleResolution": "node16", "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - }, "outDir": "./build", "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/packages/server/tsoa.json b/packages/server/tsoa.json index 0c474e8..d7ad869 100644 --- a/packages/server/tsoa.json +++ b/packages/server/tsoa.json @@ -1,7 +1,7 @@ { "entryFile": "src/index.ts", "noImplicitAdditionalProperties": "throw-on-extras", - "controllerPathGlobs": ["src/controllers/**/*Controller.ts"], + "controllerPathGlobs": ["src/controllers/**/*Controller.ts", "src/types/**/*.ts"], "spec": { "outputDirectory": "build", "specVersion": 3, @@ -13,6 +13,7 @@ "host": "localhost:3005" }, "routes": { - "routesDir": "src/routes" + "routesDir": "src/routes", + "authenticationModule": "src/middleware/auth.ts" } }