222 lines
7.9 KiB
JavaScript
222 lines
7.9 KiB
JavaScript
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()
|