Compare commits

..

2 Commits

15 changed files with 712 additions and 351 deletions

View File

@@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "tsoa spec-and-routes && tsc && tsc-alias", "build": "tsoa -c tsoaConfig.json spec-and-routes && tsc && tsc-alias",
"serve": "node ./build/index.js", "serve": "node ./build/index.js",
"start": "nodemon" "start": "nodemon"
}, },
@@ -19,7 +19,7 @@
"nocache": "^4.0.0", "nocache": "^4.0.0",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"tsoa": "^5.0.0", "tsoa": "^6.6.0",
"swagger-ui-express": "^5.0.0" "swagger-ui-express": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -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<UserInfo[]> => {
try {
const response = await axios.post<ApiResponse<UserInfo[]>>(
"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<ApiResponse<UserInfo[]>>(200, 'Success')
@Response<ApiResponse<null>>(400, 'Bad Request')
@Response<ApiResponse<null>>(401, 'Unauthorized')
public async getGroupUsers(@Request() req: ExpressRequest): Promise<ApiResponse<UserInfo[]>> {
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,
};
}
}

View File

@@ -1,5 +1,5 @@
import { Controller, Get, Route, Response, Tags } from 'tsoa'; import { Controller, Get, Route, Response, Tags } from 'tsoa';
import { ApiResponse } from '../types/api'; import type { ApiResponse } from '../types/api';
@Route('api') @Route('api')
@Tags('Test') @Tags('Test')
@@ -13,6 +13,7 @@ export class TestController extends Controller {
@Response<ApiResponse>(200, 'Success') @Response<ApiResponse>(200, 'Success')
public async getTest(): Promise<ApiResponse> { public async getTest(): Promise<ApiResponse> {
return { return {
code: 200,
message: 'success', message: 'success',
data: null, data: null,
}; };

View File

@@ -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<ApiResponse<UserInfo>>(200, 'Success')
@Response(401, 'Unauthorized')
public async getUserGroupInfo(@Request() req: ExpressRequest): Promise<ApiResponse<UserInfo>> {
return {
code: 200,
message: 'success',
data: req.user
};
}
}

View File

@@ -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<ApiResponse<UserGroupInfo>>(200, 'Success')
@Response(401, 'Unauthorized')
public async getUserGroupInfo(@Request() req: ExpressRequest): Promise<ApiResponse<UserGroupInfo>> {
const userInfo = req.userInfo;
const groupInfo = req.groupInfo;
return {
message: 'success',
data: {
user: userInfo || null,
group: groupInfo || null
}
};
}
}

View File

@@ -8,8 +8,9 @@ import path from "path";
dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
import "./database"; import "./database";
import { sequelize } from "@/database/instance"; import { sequelize } from "./database/instance";
import { RegisterTsoaRoutes } from "./middleware/tsoa.middleware"; import {errorHandler} from "./middleware/errorHandler";
import { RegisterRoutes } from "./routes/routes"; // tsoa 生成的
const port = process.env.PORT || 3005; const port = process.env.PORT || 3005;
@@ -19,11 +20,12 @@ app.use(nocache());
app.use(cors()); app.use(cors());
app.use(express.json({ limit: "100mb" })); app.use(express.json({ limit: "100mb" }));
app.use(compression()); app.use(compression());
app.use(express.static(path.resolve(__dirname, "client"))); app.use(express.static(path.resolve(__dirname, "client")));
// Register tsoa routes // Register tsoa routes
RegisterTsoaRoutes(app); RegisterRoutes(app);
app.use(errorHandler);
const host = "0.0.0.0"; const host = "0.0.0.0";

View File

@@ -1,27 +1,25 @@
import {Request, Response, NextFunction} from "express"; import {Request} from "express";
import axios from "axios"; import axios from "axios";
import {ApiError, ApiResponse} from "../types/api";
import type {UserInfo} from "../types/user";
export type UserInfo = { export async function expressAuthentication(
userId: number; request: Request,
nickname: string; securityName: string,
avatarUrl: string; scopes?: string[]
gender: 'MALE' | 'FEMALE' | 'UNKNOWN'; ): Promise<any> {
nimToken: string; // NetEase Cloud Communication token if (securityName === "jwt") {
nimAccountId: string; // NetEase Cloud Communication account ID const authHeader = request.headers.authorization;
createdAt: string; const token = extractTokenFromHeader(authHeader);
updatedAt: string;
}
declare global { if (!token) {
namespace Express { throw new ApiError(401, "Unauthorized");
interface Request {
userInfo?: UserInfo | null;
groupInfo?: {
groupId: number;
users: UserInfo[];
} | null;
} }
// 返回的对象会挂到 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 * Get user information by token
*/ */
export const getUserInfoByToken = async (token: string): Promise<any> => { export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
try { try {
const response = await axios.post( const response = await axios.post<ApiResponse<Omit<UserInfo, 'token'>>>(
"https://egret.byteawake.com/api/user/info", "https://egret.byteawake.com/api/user/info",
{}, // 请求体数据,这里为空对象 {}, // 请求体数据,这里为空对象
{ {
@@ -56,74 +54,11 @@ export const getUserInfoByToken = async (token: string): Promise<any> => {
} }
); );
return response.data; if (response.data.code !== 200 || !response.data.data) {
} catch (error: any) { throw new Error(`Failed to get user information: ${response.data.message}`);
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<void> => {
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;
} }
// Get user information return {...response.data.data, token};
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<any> => {
try {
const response = await axios.post<UserInfo[]>(
"https://egret.byteawake.com/api/group/members",
{groupId},
{
headers: {
Authorization: `Bearer ${token}`,
},
timeout: 10000, // 10 second timeout
}
);
return response.data;
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
// API returned error response // API returned error response
@@ -139,40 +74,4 @@ export const getGroupUsers = async (groupId: number, token: string): Promise<any
}; };
/**
* group information middleware
*/
export const groupInfoMiddleware = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
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,
});
}
};

View File

@@ -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' });
}

View File

@@ -1,14 +1,14 @@
import { UserInfo } from '../middleware/auth';
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code: number;
message: string; message: string;
data: T | null; data: T | null;
} }
export interface UserGroupInfo { export class ApiError extends Error {
user: UserInfo | null; status: number;
group: { constructor(status: number, message: string) {
groupId: number; super(message);
users: UserInfo[]; this.status = status;
} | null; Object.setPrototypeOf(this, ApiError.prototype);
}
} }

View File

@@ -0,0 +1,9 @@
import {UserInfo} from "./user";
declare global {
namespace Express {
interface Request {
user: UserInfo;
}
}
}

View File

@@ -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
}

View File

@@ -6,9 +6,6 @@
"module": "node16", "module": "node16",
"moduleResolution": "node16", "moduleResolution": "node16",
"baseUrl": ".", "baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./build", "outDir": "./build",
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,

View File

@@ -1,7 +1,7 @@
{ {
"entryFile": "src/index.ts", "entryFile": "src/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras", "noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/**/*Controller.ts"], "controllerPathGlobs": ["src/controllers/**/*Controller.ts", "src/types/**/*.ts"],
"spec": { "spec": {
"outputDirectory": "build", "outputDirectory": "build",
"specVersion": 3, "specVersion": 3,
@@ -13,6 +13,7 @@
"host": "localhost:3005" "host": "localhost:3005"
}, },
"routes": { "routes": {
"routesDir": "src/routes" "routesDir": "src/routes",
"authenticationModule": "src/middleware/auth.ts"
} }
} }

714
yarn.lock

File diff suppressed because it is too large Load Diff