This commit is contained in:
艾贤凌
2026-03-16 12:05:55 +08:00
parent af3a7c83e8
commit 6d4a72161f
33 changed files with 5671 additions and 178 deletions

View File

@@ -0,0 +1,221 @@
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()