Compare commits
2 Commits
9551f6aab9
...
8757f05e44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8757f05e44 | ||
|
|
d346a9f2f4 |
@@ -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": {
|
||||||
|
|||||||
68
packages/server/src/controllers/GroupController.ts
Normal file
68
packages/server/src/controllers/GroupController.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
26
packages/server/src/controllers/UserController.ts
Normal file
26
packages/server/src/controllers/UserController.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(401, "Unauthorized");
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
// 返回的对象会挂到 request.user 上
|
||||||
namespace Express {
|
return await getUserInfoByToken(token);
|
||||||
interface Request {
|
|
||||||
userInfo?: UserInfo | null;
|
|
||||||
groupInfo?: {
|
|
||||||
groupId: number;
|
|
||||||
users: UserInfo[];
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
17
packages/server/src/middleware/errorHandler.ts
Normal file
17
packages/server/src/middleware/errorHandler.ts
Normal 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' });
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
9
packages/server/src/types/express.ts
Normal file
9
packages/server/src/types/express.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import {UserInfo} from "./user";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user: UserInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/server/src/types/user.ts
Normal file
11
packages/server/src/types/user.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user