407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
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()
|
||
}
|
||
} |