import Router from 'koa-router' import mysql from '../mysql/index.js' import * as log4js from '../log4js.js' import config from '../config/index.js' import { encryptPassword } from '../utils.js' const router = new Router() function ok(ctx, data = {}, message = '操作成功') { ctx.body = { code: 0, message, ...data } } function fail(ctx, message = '操作失败', code = 1) { ctx.body = { code, message } } /** * 发起 HTTP 请求(替代 PHP curl) */ async function fetchJson(url, options = {}) { const res = await fetch(url, options) return res.json() } // ─── GET /api/linuxdo/authorize 跳转 LinuxDo 授权 ──────────────────────────── router.get('/api/linuxdo/authorize', (ctx) => { const { clientId, redirectUri, authorizeUrl } = config.linuxdo const url = `${authorizeUrl}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}` ctx.redirect(url) }) // ─── GET /api/linuxdo/callback LinuxDo OAuth 回调 ──────────────────────────── router.get('/api/linuxdo/callback', async (ctx) => { const { code } = ctx.query if (!code) { ctx.status = 400 return fail(ctx, '缺少 code 参数') } const { clientId, clientSecret, redirectUri, tokenUrl, userUrl } = config.linuxdo // 1. 换取 access_token const tokenKey = Buffer.from(`${clientId}:${clientSecret}`).toString('base64') let tokenData try { tokenData = await fetchJson(tokenUrl, { method: 'POST', headers: { 'Authorization': `Basic ${tokenKey}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri, }).toString(), }) } catch (e) { log4js.koa.error('LinuxDo 获取 token 失败', e.message) return ctx.redirect('/?error=token_failed') } if (!tokenData?.access_token) { log4js.koa.warn('LinuxDo token 异常', JSON.stringify(tokenData)) return ctx.redirect('/?error=token_invalid') } // 2. 获取用户信息 let userInfo try { userInfo = await fetchJson(userUrl, { headers: { 'Authorization': `Bearer ${tokenData.access_token}` }, }) } catch (e) { return ctx.redirect('/?error=user_failed') } const connectId = userInfo?.username if (!connectId) return ctx.redirect('/?error=user_invalid') // 3. 查找本地绑定关系 const [bindRows] = await mysql.query( 'SELECT username FROM player_connect_threeparty WHERE type = ? AND connect_id = ? LIMIT 1', ['linuxdo', connectId] ) if (bindRows.length > 0) { // 已绑定:直接查账号密码并跳转游戏 const [playerRows] = await mysql.query( 'SELECT username, password FROM player WHERE username = ? LIMIT 1', [bindRows[0].username] ) if (playerRows.length > 0) { const p = playerRows[0] return ctx.redirect(`/play?account=${p.username}&token=${p.password}`) } } // 4. 未绑定:跳转到前端绑定页面,携带 connect_id return ctx.redirect(`/linuxdo-bind?connect_id=${encodeURIComponent(connectId)}`) }) // ─── POST /api/linuxdo/bind 绑定 LinuxDo 账号 ──────────────────────────────── router.post('/api/linuxdo/bind', async (ctx) => { const { account, password, connect_id, action } = ctx.request.body if (!connect_id) return fail(ctx, '缺少 connect_id') if (action === 'register') { // 自动用 LinuxDo 账号名注册 const username = connect_id const pwd = encryptPassword('linuxdo_' + connect_id) // 尝试注册,如果账号已存在则直接绑定 const [existRows] = await mysql.query('SELECT id FROM player WHERE username = ?', [username]) if (!existRows.length) { await mysql.query( 'INSERT INTO player (username, password, server_id, reg_time, reg_ip) VALUES (?, ?, 1, NOW(), "linuxdo")', [username, pwd] ) } await _bindConnect(username, 'linuxdo', connect_id) const [player] = await mysql.query('SELECT password FROM player WHERE username = ?', [username]) return ok(ctx, { account: username, token: player[0]?.password }, '绑定成功') } // 绑定已有账号 if (!account || !password) return fail(ctx, '请输入账号和密码') const encPwd = encryptPassword(password) const [playerRows] = await mysql.query( 'SELECT username, password FROM player WHERE username = ? AND password = ?', [account, encPwd] ) if (!playerRows.length) return fail(ctx, '账号或密码不正确!') await _bindConnect(account, 'linuxdo', connect_id) log4js.koa.info('LinuxDo 绑定成功', account, connect_id) return ok(ctx, { account, token: playerRows[0].password }, '绑定成功') }) // ─── GET /api/bind 查询当前用户的第三方绑定关系 ────────────────────────────── // 需要 JWT 鉴权(已登录状态) router.get('/api/bind', async (ctx) => { const account = ctx.user?.username if (!account) return fail(ctx, '未登录', 401) const [rows] = await mysql.query( 'SELECT type, connect_id, created_at FROM player_connect_threeparty WHERE username = ?', [account] ) const bindings = {} for (const row of rows) { bindings[row.type] = { connectId: row.connect_id, createdAt: row.created_at, } } return ok(ctx, { bindings }, '查询成功') }) // ─── POST /api/bind_account 绑定第三方账号(游戏服务端内部回调)────────────── // 对应 PHP api.php case 'bind':写入 player_connect_threeparty,返回账号密码哈希 // 注意:此接口供游戏服务端内部使用,不需要 JWT(在 auth.js 白名单中配置) router.post('/api/bind_account', async (ctx) => { const { account, connect_id, tp_type } = ctx.request.body const tpType = tp_type || 'linuxdo' if (!account || !connect_id) return fail(ctx, '参数错误') // 检查账号是否存在 const [playerRows] = await mysql.query( 'SELECT password FROM player WHERE username = ? LIMIT 1', [account] ) if (!playerRows.length) return fail(ctx, '账号不存在') // 写入绑定关系(IGNORE 防止重复) await mysql.query( 'INSERT IGNORE INTO player_connect_threeparty (username, type, connect_id) VALUES (?, ?, ?)', [account, tpType, connect_id] ) log4js.koa.info('bind_account', account, tpType, connect_id) // 兼容旧版 PHP 返回:{ password: md5哈希 } return ok(ctx, { password: playerRows[0].password }, '绑定成功') }) // ─── GET /api/link 按第三方 connect_id 查询绑定的本地账号 ────────────────────── // 对应 PHP api.php case 'link':通过 linuxdo connect_id 反查本地账号 // 供游戏服务端内部使用,不需要 JWT router.get('/api/link', async (ctx) => { const { connect_id, tp_type } = ctx.query const tpType = tp_type || 'linuxdo' if (!connect_id) return fail(ctx, '参数错误') const [bindRows] = await mysql.query( 'SELECT username FROM player_connect_threeparty WHERE type = ? AND connect_id = ? LIMIT 1', [tpType, connect_id] ) if (!bindRows.length) return fail(ctx, '未绑定', 1) const [playerRows] = await mysql.query( 'SELECT username, password FROM player WHERE username = ? LIMIT 1', [bindRows[0].username] ) if (!playerRows.length) return fail(ctx, '账号不存在') return ok(ctx, { data: { username: playerRows[0].username, password: playerRows[0].password } }, '查询成功') }) async function _bindConnect(username, type, connectId) { // upsert:存在则忽略 await mysql.query( 'INSERT IGNORE INTO player_connect_threeparty (username, type, connect_id) VALUES (?, ?, ?)', [username, type, connectId] ) } export default router.routes()