feat(platform): Implement legacy system integration with Wrapper Bridge architecture
Complete integration of the old clinical research platform (www.xunzhengyixue.com) into the new AI platform via Token injection + iframe embedding: Backend: - Add legacy-bridge module (MySQL pool, auth service, routes) - POST /api/v1/legacy/auth: JWT -> phone lookup -> Token injection into old MySQL - Auto-create user in old system if not found (matched by phone number) Frontend: - LegacySystemPage: iframe container with Bridge URL construction - ResearchManagement + StatisticalTools entry components - Module registry updated from external links to iframe embed mode ECS (token-bridge.html deployed to www.xunzhengyixue.com): - Wrapper Bridge: sets cookies within same-origin context - Storage Access API for cross-site dev environments - CSS injection: hide old system nav/footer, remove padding gaps - Inner iframe loads target page with full DOM access (same-origin) Key technical decisions: - Token injection (direct MySQL write) instead of calling login API - Wrapper Bridge instead of parent-page cookie setting (cross-origin fix) - Storage Access API + SameSite=None;Secure for third-party cookie handling - User isolation guaranteed by phone number matching Documentation: - Integration plan v4.0 with full implementation record - Implementation summary with 6 pitfalls documented - System status guide updated (ST module now integrated) Tested: Local E2E verified - auto login, research management, 126 statistical tools, report generation, download, UI layout all working correctly Made-with: Cursor
This commit is contained in:
111
backend/package-lock.json
generated
111
backend/package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"jsonrepair": "^3.13.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jspdf": "^3.0.3",
|
||||
"mysql2": "^3.18.2",
|
||||
"openai": "^6.16.0",
|
||||
"p-queue": "^9.0.1",
|
||||
"pg-boss": "^12.5.2",
|
||||
@@ -1458,6 +1459,15 @@
|
||||
"fastq": "^1.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
|
||||
@@ -2902,6 +2912,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -3302,6 +3321,12 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -3766,6 +3791,27 @@
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.7.2.tgz",
|
||||
@@ -3938,6 +3984,44 @@
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.18.2",
|
||||
"resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.18.2.tgz",
|
||||
"integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2/node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
|
||||
@@ -3949,6 +4033,18 @@
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
@@ -5180,6 +5276,21 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sql-escaper": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz",
|
||||
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0",
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"jsonrepair": "^3.13.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jspdf": "^3.0.3",
|
||||
"mysql2": "^3.18.2",
|
||||
"openai": "^6.16.0",
|
||||
"p-queue": "^9.0.1",
|
||||
"pg-boss": "^12.5.2",
|
||||
|
||||
120
backend/scripts/test-legacy-auth.ts
Normal file
120
backend/scripts/test-legacy-auth.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Legacy Auth Token Injection Test
|
||||
*
|
||||
* Tests the full flow:
|
||||
* 1. Connect to old system MySQL (xzyx_online)
|
||||
* 2. Find user by phone
|
||||
* 3. Generate MD5 token (same formula as old Java system)
|
||||
* 4. Insert token into u_user_token
|
||||
* 5. Print cookie-setting instructions for manual browser verification
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/test-legacy-auth.ts <phone>
|
||||
* npx tsx scripts/test-legacy-auth.ts 18611348738
|
||||
*/
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const MYSQL_CONFIG = {
|
||||
host: process.env.LEGACY_MYSQL_HOST || '8.154.22.149',
|
||||
port: parseInt(process.env.LEGACY_MYSQL_PORT || '3306'),
|
||||
user: process.env.LEGACY_MYSQL_USER || 'xzyx_rw',
|
||||
password: process.env.LEGACY_MYSQL_PASSWORD || 'SKJfdwalkd',
|
||||
database: process.env.LEGACY_MYSQL_DATABASE || 'xzyx_online',
|
||||
};
|
||||
|
||||
function generateToken(userId: number): string {
|
||||
const timestamp = Date.now();
|
||||
const raw = `KyKz1.0:${userId}:${timestamp}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const phone = process.argv[2];
|
||||
if (!phone) {
|
||||
console.error('Usage: npx tsx scripts/test-legacy-auth.ts <phone>');
|
||||
console.error('Example: npx tsx scripts/test-legacy-auth.ts 18611348738');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('Legacy Auth Token Injection Test');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// Step 1: Connect
|
||||
console.log('\n[1/5] Connecting to MySQL...');
|
||||
const conn = await mysql.createConnection(MYSQL_CONFIG);
|
||||
console.log(` OK - connected to ${MYSQL_CONFIG.host}:${MYSQL_CONFIG.port}/${MYSQL_CONFIG.database}`);
|
||||
|
||||
// Step 2: Find user
|
||||
console.log(`\n[2/5] Looking up user by phone: ${phone}`);
|
||||
const [rows] = await conn.execute<any[]>(
|
||||
'SELECT id, phone, nickname, real_name, user_role FROM u_user_info WHERE phone = ?',
|
||||
[phone],
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log(' User NOT found. To test, pick an existing user:');
|
||||
const [sample] = await conn.execute<any[]>(
|
||||
'SELECT id, phone, nickname, real_name FROM u_user_info LIMIT 5',
|
||||
);
|
||||
console.table(sample);
|
||||
await conn.end();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const user = rows[0];
|
||||
console.log(` Found: id=${user.id}, nickname="${user.nickname}", role=${user.user_role}`);
|
||||
|
||||
// Step 3: Generate token
|
||||
console.log('\n[3/5] Generating MD5 token...');
|
||||
const token = generateToken(user.id);
|
||||
console.log(` Token: ${token}`);
|
||||
|
||||
// Step 4: Delete old tokens + insert new one
|
||||
// gen_time must be milliseconds since epoch (Java System.currentTimeMillis()),
|
||||
// NOT MySQL NOW(). The old system's isTimeout() does millisecond arithmetic.
|
||||
console.log('\n[4/5] Deleting old tokens and inserting new one...');
|
||||
await conn.execute('DELETE FROM u_user_token WHERE user_id = ?', [user.id]);
|
||||
const genTime = Date.now();
|
||||
await conn.execute(
|
||||
'INSERT INTO u_user_token (user_id, token, gen_time, user_role) VALUES (?, ?, ?, ?)',
|
||||
[user.id, token, genTime, user.user_role],
|
||||
);
|
||||
console.log(` OK - token inserted (gen_time=${genTime}, which is ${new Date(genTime).toISOString()})`);
|
||||
|
||||
// Step 5: Verify
|
||||
console.log('\n[5/5] Verifying token in database...');
|
||||
const [verify] = await conn.execute<any[]>(
|
||||
'SELECT id, user_id, token, gen_time, user_role FROM u_user_token WHERE token = ?',
|
||||
[token],
|
||||
);
|
||||
console.log(' Verified:');
|
||||
console.table(verify);
|
||||
|
||||
await conn.end();
|
||||
|
||||
// Print manual test instructions
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('TOKEN INJECTION SUCCESSFUL!');
|
||||
console.log('='.repeat(60));
|
||||
console.log('\nManual browser test:');
|
||||
console.log('1. Open browser, go to https://www.xunzhengyixue.com');
|
||||
console.log('2. Open DevTools (F12) -> Console');
|
||||
console.log('3. Paste the following 3 lines:\n');
|
||||
|
||||
const nickname = user.nickname || user.real_name || phone;
|
||||
console.log(`document.cookie = "token=${token}; domain=.xunzhengyixue.com; path=/";`);
|
||||
console.log(`document.cookie = "nickname=${encodeURIComponent(nickname)}; domain=.xunzhengyixue.com; path=/";`);
|
||||
console.log(`document.cookie = "id=${user.id}; domain=.xunzhengyixue.com; path=/";`);
|
||||
|
||||
console.log('\n4. Refresh the page (F5)');
|
||||
console.log('5. You should see the user\'s projects without logging in manually');
|
||||
console.log('\n' + '='.repeat(60));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -214,6 +214,13 @@ import { ssaRoutes } from './modules/ssa/index.js';
|
||||
await fastify.register(ssaRoutes, { prefix: '/api/v1/ssa' });
|
||||
logger.info('✅ SSA智能统计分析路由已注册: /api/v1/ssa');
|
||||
|
||||
// ============================================
|
||||
// 【集成模块】Legacy Bridge - 旧系统集成桥接
|
||||
// ============================================
|
||||
import { legacyBridgeRoutes } from './modules/legacy-bridge/routes.js';
|
||||
await fastify.register(legacyBridgeRoutes, { prefix: '/api/v1/legacy' });
|
||||
logger.info('✅ Legacy Bridge路由已注册: /api/v1/legacy');
|
||||
|
||||
// 启动服务器
|
||||
const start = async () => {
|
||||
try {
|
||||
@@ -340,6 +347,11 @@ const gracefulShutdown = async (signal: string) => {
|
||||
await prisma.$disconnect();
|
||||
console.log('✅ 数据库连接已关闭');
|
||||
|
||||
// 4. 关闭 Legacy MySQL 连接池
|
||||
const { closeLegacyMysqlPool } = await import('./modules/legacy-bridge/mysql-pool.js');
|
||||
await closeLegacyMysqlPool();
|
||||
console.log('✅ Legacy MySQL连接池已关闭');
|
||||
|
||||
console.log('👋 优雅关闭完成,再见!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
||||
118
backend/src/modules/legacy-bridge/legacy-auth.service.ts
Normal file
118
backend/src/modules/legacy-bridge/legacy-auth.service.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Legacy System Auth Service
|
||||
*
|
||||
* Handles token injection into the old Java system's MySQL database.
|
||||
* Generates MD5 tokens matching the old system's formula and inserts
|
||||
* them into u_user_token, bypassing the login API entirely.
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { getLegacyMysqlPool } from './mysql-pool.js';
|
||||
import { logger } from '../../common/logging/index.js';
|
||||
|
||||
interface LegacyUser {
|
||||
id: number;
|
||||
phone: string;
|
||||
nickname: string;
|
||||
real_name: string;
|
||||
user_role: string;
|
||||
}
|
||||
|
||||
interface LegacyAuthResult {
|
||||
token: string;
|
||||
nickname: string;
|
||||
id: number;
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PASSWORD_MD5 = 'E10ADC3949BA59ABBE56E057F20F883E'; // MD5 of "123456"
|
||||
|
||||
/**
|
||||
* Token formula from old system: MD5("KyKz1.0:" + userId + ":" + timestamp)
|
||||
*/
|
||||
function generateLegacyToken(userId: number): string {
|
||||
const timestamp = Date.now();
|
||||
const raw = `KyKz1.0:${userId}:${timestamp}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user in old system by phone number.
|
||||
*/
|
||||
async function findUserByPhone(phone: string): Promise<LegacyUser | null> {
|
||||
const pool = getLegacyMysqlPool();
|
||||
const [rows] = await pool.execute<any[]>(
|
||||
'SELECT id, phone, nickname, real_name, user_role FROM u_user_info WHERE phone = ?',
|
||||
[phone],
|
||||
);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user in old system with default password.
|
||||
*/
|
||||
async function createUser(phone: string, realName: string): Promise<LegacyUser> {
|
||||
const pool = getLegacyMysqlPool();
|
||||
const [result] = await pool.execute<any>(
|
||||
`INSERT INTO u_user_info
|
||||
(phone, nickname, real_name, hospital, company, academy, user_role, user_status, password, hosptial_id, email_validate_status, register_time)
|
||||
VALUES (?, ?, ?, '本地', '', '', 'NORMAL', 'NORMAL', ?, 0, 0, UNIX_TIMESTAMP())`,
|
||||
[phone, realName || phone, realName || phone, DEFAULT_PASSWORD_MD5],
|
||||
);
|
||||
|
||||
const insertId = result.insertId;
|
||||
logger.info('Created legacy user', { phone, insertId });
|
||||
|
||||
return {
|
||||
id: insertId,
|
||||
phone,
|
||||
nickname: realName || phone,
|
||||
real_name: realName || phone,
|
||||
user_role: 'NORMAL',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a fresh token into old system's u_user_token table.
|
||||
* gen_time must be Java System.currentTimeMillis() (milliseconds since epoch),
|
||||
* NOT MySQL NOW(). The old system's isTimeout() does millisecond arithmetic.
|
||||
* Also delete existing tokens for this user first (matches Java generateToken behavior).
|
||||
*/
|
||||
async function injectToken(userId: number, token: string, userRole: string): Promise<void> {
|
||||
const pool = getLegacyMysqlPool();
|
||||
await pool.execute('DELETE FROM u_user_token WHERE user_id = ?', [userId]);
|
||||
await pool.execute(
|
||||
`INSERT INTO u_user_token (user_id, token, gen_time, user_role) VALUES (?, ?, ?, ?)`,
|
||||
[userId, token, Date.now(), userRole],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry: authenticate a user in the old system.
|
||||
* - Finds or creates the user by phone
|
||||
* - Generates and injects a token
|
||||
* - Returns credentials for cookie-setting on the frontend
|
||||
*/
|
||||
export async function authenticateLegacyUser(
|
||||
phone: string,
|
||||
displayName?: string,
|
||||
): Promise<LegacyAuthResult> {
|
||||
let user = await findUserByPhone(phone);
|
||||
|
||||
if (!user) {
|
||||
logger.info('Legacy user not found, creating', { phone });
|
||||
user = await createUser(phone, displayName || '');
|
||||
}
|
||||
|
||||
const token = generateLegacyToken(user.id);
|
||||
await injectToken(user.id, token, user.user_role);
|
||||
|
||||
logger.info('Legacy token injected', { phone, userId: user.id });
|
||||
|
||||
return {
|
||||
token,
|
||||
nickname: user.nickname || user.real_name || phone,
|
||||
id: user.id,
|
||||
userRole: user.user_role,
|
||||
};
|
||||
}
|
||||
39
backend/src/modules/legacy-bridge/mysql-pool.ts
Normal file
39
backend/src/modules/legacy-bridge/mysql-pool.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Legacy System MySQL Connection Pool
|
||||
*
|
||||
* Connects to the old Java system's MariaDB/MySQL database (xzyx_online)
|
||||
* on ECS 8.154.22.149 for user sync and token injection.
|
||||
*/
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import { logger } from '../../common/logging/index.js';
|
||||
|
||||
const LEGACY_MYSQL_CONFIG = {
|
||||
host: process.env.LEGACY_MYSQL_HOST || '8.154.22.149',
|
||||
port: parseInt(process.env.LEGACY_MYSQL_PORT || '3306'),
|
||||
user: process.env.LEGACY_MYSQL_USER || 'xzyx_rw',
|
||||
password: process.env.LEGACY_MYSQL_PASSWORD || 'SKJfdwalkd',
|
||||
database: process.env.LEGACY_MYSQL_DATABASE || 'xzyx_online',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 5,
|
||||
queueLimit: 0,
|
||||
connectTimeout: 10000,
|
||||
};
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getLegacyMysqlPool(): mysql.Pool {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(LEGACY_MYSQL_CONFIG);
|
||||
logger.info('Legacy MySQL pool created', { host: LEGACY_MYSQL_CONFIG.host });
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function closeLegacyMysqlPool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
logger.info('Legacy MySQL pool closed');
|
||||
}
|
||||
}
|
||||
61
backend/src/modules/legacy-bridge/routes.ts
Normal file
61
backend/src/modules/legacy-bridge/routes.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Legacy Bridge Routes
|
||||
*
|
||||
* POST /api/v1/legacy/auth - Authenticate current user in the old Java system
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { authenticate } from '../../common/auth/auth.middleware.js';
|
||||
import { authenticateLegacyUser } from './legacy-auth.service.js';
|
||||
import { logger } from '../../common/logging/index.js';
|
||||
import { prisma } from '../../config/database.js';
|
||||
|
||||
export async function legacyBridgeRoutes(fastify: FastifyInstance): Promise<void> {
|
||||
/**
|
||||
* POST /auth
|
||||
*
|
||||
* Uses the current user's JWT to generate a valid token for the old system.
|
||||
* Frontend should set the returned values as cookies on .xunzhengyixue.com
|
||||
*/
|
||||
fastify.post('/auth', {
|
||||
preHandler: [authenticate],
|
||||
}, async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
const { userId, phone } = request.user!;
|
||||
|
||||
if (!phone) {
|
||||
return reply.status(400).send({
|
||||
error: 'Bad Request',
|
||||
message: '当前用户无手机号,无法同步到旧系统',
|
||||
});
|
||||
}
|
||||
|
||||
// Look up display name from new system's user table
|
||||
let displayName = phone;
|
||||
try {
|
||||
const newUser = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { name: true },
|
||||
});
|
||||
if (newUser?.name) {
|
||||
displayName = newUser.name;
|
||||
}
|
||||
} catch {
|
||||
// Non-critical, fall back to phone
|
||||
}
|
||||
|
||||
const result = await authenticateLegacyUser(phone, displayName);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Legacy auth failed', { error });
|
||||
return reply.status(500).send({
|
||||
error: 'Internal Server Error',
|
||||
message: '旧系统认证失败,请稍后重试',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
120
backend/src/modules/legacy-bridge/token-bridge.html
Normal file
120
backend/src/modules/legacy-bridge/token-bridge.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>加载中...</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; }
|
||||
#frame { width: 100%; height: 100%; border: none; display: none; }
|
||||
#status { display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
height: 100%; gap: 16px; background: #f5f7fa; }
|
||||
#status .spinner { width: 32px; height: 32px; border: 3px solid #e0e0e0;
|
||||
border-top-color: #4f6ef7; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
#status p { color: #888; font-size: 14px; }
|
||||
#status button { padding: 10px 28px; font-size: 15px; border-radius: 8px; border: none;
|
||||
background: #4f6ef7; color: #fff; cursor: pointer; transition: background 0.2s; }
|
||||
#status button:hover { background: #3a5ce5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status">
|
||||
<div class="spinner"></div>
|
||||
<p id="msg">正在连接...</p>
|
||||
<button id="btn" style="display:none;">点击授权并继续</button>
|
||||
</div>
|
||||
<iframe id="frame"></iframe>
|
||||
<script>
|
||||
(function() {
|
||||
var P = new URLSearchParams(location.search);
|
||||
var token = P.get('token');
|
||||
var nickname = P.get('nickname');
|
||||
var id = P.get('id');
|
||||
var role = P.get('userRole');
|
||||
var redirect = P.get('redirect') || '/index.html';
|
||||
|
||||
var cookieOpt = '; domain=.xunzhengyixue.com; path=/; SameSite=None; Secure';
|
||||
|
||||
function setCookies() {
|
||||
if (!token) return;
|
||||
document.cookie = 'token=' + encodeURIComponent(token) + cookieOpt;
|
||||
document.cookie = 'nickname=' + encodeURIComponent(nickname) + cookieOpt;
|
||||
document.cookie = 'id=' + encodeURIComponent(id) + cookieOpt;
|
||||
document.cookie = 'userRole=' + encodeURIComponent(role) + cookieOpt;
|
||||
}
|
||||
|
||||
// ---- Custom CSS injected into every page loaded in the inner iframe ----
|
||||
var CUSTOM_CSS = [
|
||||
'#header-navbar { display: none !important; }',
|
||||
'#footer-bar { display: none !important; }',
|
||||
'#menu { display: none !important; }',
|
||||
'#footer { display: none !important; }',
|
||||
'body { padding-top: 0 !important; margin-top: 0 !important; }',
|
||||
'#page-wrapper { padding-top: 0 !important; margin-top: 0 !important; }',
|
||||
'.navbar-fixed-top + * { margin-top: 0 !important; padding-top: 0 !important; }',
|
||||
'section#portfolio { padding-top: 10px !important; }',
|
||||
].join('\n');
|
||||
|
||||
function showFrame() {
|
||||
var frame = document.getElementById('frame');
|
||||
document.getElementById('status').style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
|
||||
frame.onload = function() {
|
||||
try {
|
||||
var doc = frame.contentDocument;
|
||||
if (!doc || doc.getElementById('_bridge_css')) return;
|
||||
var s = doc.createElement('style');
|
||||
s.id = '_bridge_css';
|
||||
s.textContent = CUSTOM_CSS;
|
||||
(doc.head || doc.documentElement).appendChild(s);
|
||||
} catch(e) {}
|
||||
};
|
||||
frame.src = redirect;
|
||||
}
|
||||
|
||||
function grantAndProceed() {
|
||||
if (document.requestStorageAccess) {
|
||||
document.requestStorageAccess()
|
||||
.then(function() { setCookies(); showFrame(); })
|
||||
.catch(function() { setCookies(); showFrame(); });
|
||||
} else {
|
||||
setCookies();
|
||||
showFrame();
|
||||
}
|
||||
}
|
||||
|
||||
function showGrantButton() {
|
||||
document.querySelector('#status .spinner').style.display = 'none';
|
||||
document.getElementById('msg').textContent = '需要授权以访问旧系统';
|
||||
var btn = document.getElementById('btn');
|
||||
btn.style.display = 'inline-block';
|
||||
btn.onclick = function() {
|
||||
btn.style.display = 'none';
|
||||
document.querySelector('#status .spinner').style.display = 'block';
|
||||
document.getElementById('msg').textContent = '正在连接...';
|
||||
grantAndProceed();
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Entry point: check storage access, then set cookies + load frame ----
|
||||
if (document.hasStorageAccess) {
|
||||
document.hasStorageAccess().then(function(has) {
|
||||
if (has) {
|
||||
setCookies();
|
||||
showFrame();
|
||||
} else {
|
||||
showGrantButton();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setCookies();
|
||||
showFrame();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user