import * as PlayerModel from '../models/player.js' import { createHash } from 'crypto' import { generateToken, uuidToNoSymboUUID } from '../generator.js' import { getOverrideHandler, getOverridePreHandler } from '../config.js' import { Token } from '../models/token.js' export const authenticate = { method: 'POST', url: '/authserver/authenticate', schema: { summary: "登录", description: "登陆账号,用于获取 accessToken。如果账号没有绑定 Telegram 则会被禁止登陆。agent.name 必须为 'minecraft'。详情请见 authlib-injector 文档。", tags: [ "Authserver" ], body: { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "clientToken": { "type": "string" }, "requestUser": { "type": "boolean" }, "agent": { "type": "object", "properties": { "name": { "type": "string" }, "version": { "type": "integer" } } } } }, response: { 200: { "type": "object", "properties": { "accessToken": { "type": "string" }, "clientToken": { "type": "string" }, "availableProfiles": { "type": "array", "items": [PlayerModel.PlayerSeriliazationSchema] }, "selectedProfile": PlayerModel.PlayerSeriliazationSchema, "user": PlayerModel.PlayerAccountSerializationSchema } } } }, preHandler: getOverridePreHandler("/authserver/authenticate"), handler: getOverrideHandler("/authserver/authenticate") ?? async function (req, rep) { let { username, password, clientToken, requestUser, agent } = req.body if( !agent || agent.name.toLowerCase() !== 'minecraft') { return await rep.code(418).send({ error: "ForbiddenOperationException", errorMessage: "无效应用名,此服务端仅支援 Minecraft", cause: "此服务器只支持 agent.name: minecraft" }) } const player = await PlayerModel.Player.findOne({ email: username, password: createHash('sha256').update(password).digest().toString('hex').toLowerCase() }) if(!player || !player.permissions.some((it) => { return it.node === 'login' && it.allowed && (it.duration === 0 || it.startDate + it.duration > Date.now()) })) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "用户名或密码错误", cause: "用户名或密码错误" }) } if(!player.telegramBind || !player.telegramBind.verified) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "未绑定 Telegram 账号或账号验证未通过,登录请求已禁止", cause: "未绑定 Telegram 账号或账号验证未通,登录请求已禁止" }) } if(!clientToken) { clientToken = createHash('sha256').update( "" + Math.random() * 1.048596).digest().toString('hex') } const [token, key] = await generateToken(clientToken, requestUser, agent) this.log.info(`/authserver/authenticate > 为玩家 ${username} 生成令牌: ${token} | 随机 key = ${key}`) const account = { id: uuidToNoSymboUUID(player.uuid), properties: [ { preferredLanguage: "zh_CN" } ] } const profile = PlayerModel.getPlayerSerialization(player) new Token({ uuid: player.uuid, token: token, clientToken: clientToken, expireDate: Date.now() + 1000 * 60 * 60 * 24 * 15, deadDate: Date.now() + 1000 * 60 * 60 * 24 * 30, }).save() return await rep.send({ accessToken: token, clientToken: clientToken, availableProfiles: [ profile ], selectedProfile: profile, user: account }) } } export const refresh = { method: 'POST', url: '/authserver/refresh', schema: { summary: "刷新令牌", description: "可以刷新的令牌仅限令牌处于 'limbo(半吊销状态)',默认为3天有效期,3天后进入 'limbo',7天后进入 'dead(失效)'。详情请见 authlib-injector 文档。", tags: [ "Authserver" ], body: { "type": "object", "properties": { "accessToken": { "type": "string" }, "clientToken": { "type": "string", "optional": true }, "requestUser": { "type": "boolean", "optional": true }, "selectedProfile": { "optional": true, ...PlayerModel.PlayerSeriliazationSchema } } }, response: { 200: { "type": "object", "properties": { "accessToken": { "type": "string" }, "clientToken": { "type": "string" }, "selectedProfile": PlayerModel.PlayerSeriliazationSchema, "user": PlayerModel.PlayerAccountSerializationSchema } } } }, preHandler: getOverridePreHandler("/authserver/refresh"), handler: getOverrideHandler("/authserver/authenticate") ?? async function (req, rep) { const { accessToken, clientToken, requestUser, selectedProfile } = req.body const query = { token: accessToken } if(clientToken) { query.clientToken = clientToken } const token = await Token.findOne(query) if(!token) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "accessToken无效", cause: "accessToken无效" }) } const { deadDate, uuid } = token if(deadDate < Date.now()) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "accessToken无效", cause: "accessToken无效" }) } const [newToken, key] = generateToken(token.uuid) this.log.info(`/authserver/authenticate > 为玩家 ${token.uuid} 刷新令牌: ${token.uuid} 为 ${newToken} | 随机 key = ${key}`) await Token.updateOne({ token: accessToken, clientToken: clientToken ?? undefined }, { $set: { expireDate: 0, deadDate: 0 } }) new Token({ uuid: uuid, token: newToken, clientToken: clientToken ?? token.clientToken, expireDate: Date.now() + 1000 * 60 * 60 * 24 * 15, deadDate: Date.now() + 1000 * 60 * 60 * 24 * 30, }).save() const response = { accessToken: newToken, clientToken: clientToken ?? token.clientToken, } if(requestUser || selectedProfile) { const player = await PlayerModel.Player.findOne({ uuid }) if(requestUser) { response.user = { id: uuidToNoSymboUUID(player.uuid), properties: [ { name: "preferredLanguage", value: "zh_CN" } ] } } if(selectedProfile) { response.selectedProfile = PlayerModel.getPlayerSerialization(player) } } return await rep.send(response) } } export const validate = { method: 'POST', url: '/authserver/validate', schema: { summary: "验证令牌", description: "验证令牌是否有效,若携带 clientToken 则会检查 clientToken是否正确,否则不会检查。详情请见 authlib-injector 文档。", tags: [ "Authserver" ], body: { "type": "object", "properties": { "accessToken": { "type": "string" }, "clientToken": { "type": "string", "optional": true } } }, response: { 204: { "type": "null" } } }, preHandler: getOverridePreHandler("/authserver/validate"), handler: getOverrideHandler("/authserver/validate") ?? async function (req, rep) { const { accessToken, clientToken } = req.body const query = { token: accessToken } if(clientToken) { query.clientToken = clientToken } const token = await Token.findOne(query) if(!token) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "accessToken无效", cause: "accessToken无效" }) } const { expireDate } = token if(expireDate < Date.now()) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "accessToken无效", cause: "accessToken无效" }) } return await rep.code(204).send() } } export const invalidate = { method: 'POST', url: '/authserver/invalidate', schema: { summary: "主动吊销令牌", description: "吊销指定令牌,处理时根据规范永远忽略 clientToken 参数。详情请见 authlib-injector 文档。", tags: [ "Authserver" ], body: { "type": "object", "properties": { "accessToken": { "type": "string" }, "clientToken": { "type": "string", "optional": true } } }, response: { 204: { "type": "null" } } } , preHandler: getOverridePreHandler("/authserver/invalidate"), handler: getOverrideHandler("/authserver/authenticate") ?? async function (req, rep) { const { accessToken } = req.body const { modifiedCount } = await Token.updateOne({ token: accessToken }, { $set: { expireDate: 0, deadDate: 0 } }) if(modifiedCount === 0) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "accessToken无效", cause: "accessToken无效" }) } return await rep.code(204).send() } } export const signout = { method: 'POST', url: '/authserver/signout', schema: { summary: "登出", description: "登出账号,吊销所有令牌。详情请见 authlib-injector 文档。", tags: [ "Authserver" ], body: { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string", "optional": true } } }, response: { 204: { "type": "null" } } }, preHandler: getOverridePreHandler("/authserver/signout"), handler: getOverrideHandler("/authserver/signout") ?? async function (req, rep) { const { username, password } = req.body const player = await PlayerModel.Player.findOne({ email: username, password: createHash('sha256').update(password).digest().toString('hex').toLowerCase() }) if(!player) { return await rep.code(401).send({ error: "Unauthorized", errorMessage: "用户名或密码错误", cause: "用户名或密码错误" }) } await Token.deleteMany({ uuid: player.uuid }) rep.code(204).send() } }