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