Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

221
server/server.ts Normal file
View File

@@ -0,0 +1,221 @@
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 app = express();
// 中间件配置
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
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) => {
console.error(err.stack);
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);
});