2026-04-29 15:18:30 +08:00
|
|
|
|
import * as express from 'express';
|
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
|
import * as morgan from 'morgan';
|
2026-05-23 22:47:34 +08:00
|
|
|
|
import { v4 as uuid } from 'uuid';
|
2026-04-29 15:18:30 +08:00
|
|
|
|
import signaling from './signaling';
|
|
|
|
|
|
import { log, LogLevel } from './log';
|
|
|
|
|
|
import Options from './class/options';
|
|
|
|
|
|
import { reset as resetHandler }from './class/httphandler';
|
|
|
|
|
|
import { initSwagger } from './swagger';
|
|
|
|
|
|
|
|
|
|
|
|
const cors = require('cors');
|
|
|
|
|
|
const multer = require('multer');
|
|
|
|
|
|
|
2026-05-23 22:47:34 +08:00
|
|
|
|
const AVATAR_UPLOAD_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
|
|
|
|
const ALLOWED_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
|
|
|
|
const ALLOWED_AVATAR_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
|
|
|
|
|
|
|
|
|
|
|
|
function safeAvatarExtension(file: any): string {
|
|
|
|
|
|
const originalExt = path.extname(file.originalname || '').toLowerCase();
|
|
|
|
|
|
if (ALLOWED_AVATAR_EXTENSIONS.has(originalExt)) {
|
|
|
|
|
|
return originalExt;
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (file.mimetype) {
|
|
|
|
|
|
case 'image/jpeg':
|
|
|
|
|
|
return '.jpg';
|
|
|
|
|
|
case 'image/png':
|
|
|
|
|
|
return '.png';
|
|
|
|
|
|
case 'image/webp':
|
|
|
|
|
|
return '.webp';
|
|
|
|
|
|
case 'image/gif':
|
|
|
|
|
|
return '.gif';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isAllowedAvatar(file: any): boolean {
|
|
|
|
|
|
const ext = path.extname(file.originalname || '').toLowerCase();
|
|
|
|
|
|
return ALLOWED_AVATAR_MIME_TYPES.has(file.mimetype) && ALLOWED_AVATAR_EXTENSIONS.has(ext);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 15:18:30 +08:00
|
|
|
|
export const createServer = (config: Options): express.Express => {
|
|
|
|
|
|
const app: express.Express = express();
|
|
|
|
|
|
resetHandler(config.mode);
|
|
|
|
|
|
// logging http access
|
|
|
|
|
|
if (config.logging != "none") {
|
|
|
|
|
|
app.use(morgan(config.logging));
|
|
|
|
|
|
}
|
|
|
|
|
|
// const signal = require('./signaling');
|
|
|
|
|
|
app.use(cors({origin: '*'}));
|
|
|
|
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
|
|
app.use(express.json());
|
2026-05-17 22:07:07 +08:00
|
|
|
|
app.get('/config', (req, res) => res.json({
|
|
|
|
|
|
useWebSocket: config.type == 'websocket',
|
|
|
|
|
|
startupMode: config.mode,
|
|
|
|
|
|
logging: config.logging,
|
|
|
|
|
|
protocol: config.secure ? 'https' : 'http',
|
|
|
|
|
|
port: config.port
|
|
|
|
|
|
}));
|
2026-04-29 15:18:30 +08:00
|
|
|
|
app.use('/signaling', signaling);
|
|
|
|
|
|
app.use(express.static(path.join(__dirname, '../client/public')));
|
|
|
|
|
|
app.use('/module', express.static(path.join(__dirname, '../client/src')));
|
|
|
|
|
|
app.get('/', (req, res) => {
|
|
|
|
|
|
const indexPagePath: string = path.join(__dirname, '../client/public/index.html');
|
|
|
|
|
|
fs.access(indexPagePath, (err) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
log(LogLevel.warn, `Can't find file ' ${indexPagePath}`);
|
|
|
|
|
|
res.status(404).send(`Can't find file ${indexPagePath}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.sendFile(indexPagePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
// 初始化Swagger
|
|
|
|
|
|
initSwagger(app, config);
|
|
|
|
|
|
|
|
|
|
|
|
// 配置multer存储
|
|
|
|
|
|
const storage = multer.diskStorage({
|
|
|
|
|
|
destination: function (req: any, file: any, cb: (error: Error | null, destination: string) => void) {
|
|
|
|
|
|
// 确保上传目录存在
|
|
|
|
|
|
const uploadDir = path.join(__dirname, '../client/public/uploads/avatars');
|
|
|
|
|
|
if (!fs.existsSync(uploadDir)) {
|
|
|
|
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
cb(null, uploadDir);
|
|
|
|
|
|
},
|
|
|
|
|
|
filename: function (req: any, file: any, cb: (error: Error | null, filename: string) => void) {
|
|
|
|
|
|
// 临时使用原始文件名,稍后在API处理中重命名
|
|
|
|
|
|
cb(null, file.originalname);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-23 22:47:34 +08:00
|
|
|
|
const upload = multer({
|
|
|
|
|
|
storage: storage,
|
|
|
|
|
|
limits: {
|
|
|
|
|
|
fileSize: AVATAR_UPLOAD_LIMIT_BYTES
|
|
|
|
|
|
},
|
|
|
|
|
|
fileFilter: (_req: express.Request, file: any, cb: (error: Error | null, acceptFile?: boolean) => void) => {
|
|
|
|
|
|
if (!isAllowedAvatar(file)) {
|
|
|
|
|
|
cb(new Error('Only jpg, png, webp, or gif avatars are allowed'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
cb(null, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-29 15:18:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 头像上传API
|
2026-05-23 22:47:34 +08:00
|
|
|
|
app.post('/api/upload/avatar', (req: express.Request, res: express.Response) => {
|
|
|
|
|
|
upload.single('avatar')(req, res, (error: Error) => {
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
log(LogLevel.warn, 'Avatar upload rejected:', error.message);
|
|
|
|
|
|
const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE';
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: isSizeLimit ? 'Avatar file is too large' : error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const request = req as any;
|
|
|
|
|
|
if (!request.file) {
|
|
|
|
|
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
|
|
|
|
|
}
|
2026-04-29 15:18:30 +08:00
|
|
|
|
|
2026-05-23 22:47:34 +08:00
|
|
|
|
const ext = safeAvatarExtension(request.file);
|
|
|
|
|
|
if (!ext) {
|
|
|
|
|
|
fs.unlink(request.file.path, () => undefined);
|
|
|
|
|
|
return res.status(400).json({ success: false, message: 'Unsupported avatar file type' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const oldPath = request.file.path;
|
|
|
|
|
|
const newFilename = `avatar_${uuid()}${ext}`;
|
|
|
|
|
|
const newPath = path.join(path.dirname(oldPath), newFilename);
|
2026-04-29 15:18:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 重命名文件
|
2026-05-23 22:47:34 +08:00
|
|
|
|
fs.rename(oldPath, newPath, (err) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
log(LogLevel.error, 'Error renaming file:', err);
|
2026-04-29 15:18:30 +08:00
|
|
|
|
return res.status(500).json({ success: false, message: '文件重命名失败' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const avatarUrl = `/uploads/avatars/${newFilename}`;
|
|
|
|
|
|
res.json({ success: true, avatarUrl: avatarUrl });
|
|
|
|
|
|
});
|
2026-05-23 22:47:34 +08:00
|
|
|
|
});
|
2026-04-29 15:18:30 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 确保uploads目录可访问
|
|
|
|
|
|
app.use('/uploads', express.static(path.join(__dirname, '../client/public/uploads')));
|
|
|
|
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
|
};
|