Files
d1kt/server/server.ts
2026-06-07 18:47:39 +08:00

257 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dotenv from 'dotenv';
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import session from 'express-session';
import MongoStore from 'connect-mongo';
import path from 'path';
import { createServer } from 'http';
import { setupMinesweeperSocket } from './websocket/minesweeperSocket';
import practiceTypesRouter from './routes/practiceTypes';
import codeExamplesRouter from './routes/codeExamples';
import authRouter from './routes/auth';
import keywordsRouter from './routes/keywords';
import practiceRecordsRouter from './routes/practiceRecords';
import leaderboardRouter from './routes/leaderboard';
import adminRoutes from './routes/admin';
import systemRoutes from './routes/system';
import oauth2Routes from './routes/oauth2.routes';
import visitorRoutes from './routes/visitor';
import vocabularyRoutes from './routes/vocabulary';
import userWordPassRouter from './routes/userWordPass';
import minesweeperRouter from './routes/minesweeper';
import towerDefenseRouter from './routes/towerDefense';
import sudokuRouter from './routes/sudoku';
// 加载环境变量
dotenv.config();
const BODY_LIMIT = process.env.BODY_LIMIT || '5mb';
const app = express();
// 中间件配置
app.use(express.json({ limit: BODY_LIMIT }));
app.use(express.urlencoded({ extended: true, limit: BODY_LIMIT }));
app.use(cors());
// 静态文件服务 - 优先处理 public 目录下的静态文件
const publicPath = path.join(__dirname, '../public');
const buildPath = path.join(__dirname, '../build');
console.log('Static paths configured:');
console.log(' Public path:', publicPath);
console.log(' Build path:', buildPath);
app.use(express.static(publicPath));
app.use(express.static(buildPath));
// 挂载塔防游戏静态目录 (使用源码中的 src 目录)
const towerDefenseStaticPath = path.join(__dirname, '../html5-tower-defense-master/src');
console.log('Tower Defense static path:', towerDefenseStaticPath);
app.use('/tower-defense', express.static(towerDefenseStaticPath));
app.get('/tower-defense', (req, res) => {
res.sendFile(path.join(towerDefenseStaticPath, 'td.html'));
});
// MongoDB 连接
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/typeskill';
// 添加 session 配置
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: MONGODB_URI,
ttl: 24 * 60 * 60
}),
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// 如果使用了代理(如 nginx添加这行
app.set('trust proxy', 1);
mongoose.connect(MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB');
})
.catch((error) => {
console.error('MongoDB connection error:', error);
});
// 路由配置
// 添加请求日志中间件 - 在所有路由之前
app.use((req, res, next) => {
console.log('=== Incoming Request ===');
console.log('Time:', new Date().toISOString());
console.log('Method:', req.method);
console.log('Path:', req.path);
console.log('Original URL:', req.originalUrl);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('========================');
next();
});
// 特殊静态 HTML 文件路由 - 在 API 路由之前处理
app.get('/xf/xf.html', (req, res) => {
const filePath = path.join(__dirname, '../public/xf/xf.html');
console.log('>>> Serving xf.html');
console.log(' File path:', filePath);
console.log(' __dirname:', __dirname);
// 检查文件是否存在
const fs = require('fs');
if (fs.existsSync(filePath)) {
console.log(' File exists: YES');
res.sendFile(filePath);
} else {
console.log(' File exists: NO');
res.status(404).send('File not found: ' + filePath);
}
});
// 通用静态 HTML 处理 - 处理 /xf/ 目录下的其他文件
app.get('/xf/*', (req, res, next) => {
const filePath = path.join(__dirname, '../public', req.path);
console.log('>>> Serving /xf/* file');
console.log(' Requested path:', req.path);
console.log(' Full file path:', filePath);
const fs = require('fs');
if (fs.existsSync(filePath)) {
console.log(' File exists: YES');
res.sendFile(filePath);
} else {
console.log(' File exists: NO, passing to next middleware');
next();
}
});
// OIDC Discovery 路由
app.get('/.well-known/openid-configuration', (req, res) => {
const issuer = process.env.ISSUER || 'https://d1kt.cn'; // 你的主域名
res.json({
issuer,
authorization_endpoint: issuer + '/api/api/oauth2/authorize',
token_endpoint: issuer + '/api/api/oauth2/token',
userinfo_endpoint: issuer + '/api/api/oauth2/userinfo',
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['HS256'],
scopes_supported: ['openid', 'profile', 'email', 'firstname', 'lastname', 'username'],
token_endpoint_auth_methods_supported: ['client_secret_post'],
claims_supported: ['sub', 'name', 'fullname', 'email', 'firstname', 'lastname', 'username'],
});
});
app.use('/api/practice-types', practiceTypesRouter);
app.use('/api/code-examples', codeExamplesRouter);
app.use('/api/auth', authRouter);
app.use('/api/oauth2', oauth2Routes);
app.use('/api/keywords', keywordsRouter);
app.use('/api/practice-records', practiceRecordsRouter);
app.use('/api/leaderboard', leaderboardRouter);
app.use('/api/admin', adminRoutes);
app.use('/api/system', systemRoutes);
app.use('/api/visitor', visitorRoutes);
app.use('/api/vocabulary', vocabularyRoutes);
app.use('/api', userWordPassRouter);
app.use('/api/minesweeper', minesweeperRouter);
app.use('/api/tower-defense', towerDefenseRouter);
app.use('/api/sudoku', sudokuRouter);
// SPA 回退路由 - 必须放在所有路由之后
app.get('*', (req, res, next) => {
console.log('>>> SPA Fallback triggered');
console.log(' Path:', req.path);
console.log(' Starts with /api/:', req.path.startsWith('/api/'));
console.log(' Includes dot:', req.path.includes('.'));
// 如果是 API 请求或静态资源,跳过
if (req.path.startsWith('/api/') || req.path.includes('.')) {
console.log(' Action: Passing to next middleware');
return next();
}
// 否则返回 React 应用的 index.html
console.log(' Action: Returning React index.html');
res.sendFile(path.join(__dirname, '../build/index.html'));
});
// 响应结束日志
app.use((req, res, next) => {
const originalSend = res.send;
const originalSendFile = res.sendFile;
res.send = function(data) {
console.log('>>> Response sent (send method)');
console.log(' Status:', res.statusCode);
console.log(' Data length:', typeof data === 'string' ? data.length : 'N/A');
return originalSend.apply(res, arguments);
};
res.sendFile = function(filePath) {
console.log('>>> Response sent (sendFile method)');
console.log(' Status:', res.statusCode);
console.log(' File:', filePath);
return originalSendFile.apply(res, arguments);
};
next();
});
// 错误处理中间件
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (res.headersSent) {
return next(err);
}
if (
err?.type === 'entity.too.large' ||
err?.name === 'PayloadTooLargeError' ||
err?.status === 413 ||
err?.statusCode === 413
) {
console.warn('Request body too large:', {
method: req.method,
path: req.path,
limit: BODY_LIMIT
});
return res.status(413).json({
error: '请求体过大,请缩小单次提交内容',
code: 'PAYLOAD_TOO_LARGE'
});
}
if (err?.type === 'entity.parse.failed') {
console.error('Invalid JSON payload:', {
method: req.method,
path: req.path,
message: err?.message
});
return res.status(400).json({
error: '请求体格式无效',
code: 'INVALID_JSON'
});
}
console.error(err?.stack || err);
res.status(500).send('Something broke!');
});
const PORT = process.env.PORT || 5001;
const httpServer = createServer(app);
// 设置 WebSocket
setupMinesweeperSocket(httpServer);
httpServer.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`HTTP Server address:`, httpServer.address());
console.log(`WebSocket server is ready`);
console.log(`Environment:`, process.env.NODE_ENV);
console.log(`REACT_APP_API_BASE_URL:`, process.env.REACT_APP_API_BASE_URL);
});