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()
|