Files
chuanqi-qycq-web/module/server/koa/linuxdo.js
艾贤凌 6d4a72161f inint
2026-03-16 12:05:55 +08:00

222 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()