From 64d632eac99c6b2ee83f3f23214b1c0ac8d668c7 Mon Sep 17 00:00:00 2001 From: azures04 Date: Wed, 24 Dec 2025 01:20:52 +0100 Subject: [PATCH] Add session service with profile and session management Introduces sessionService.js providing functions for legacy session registration and validation, profile retrieval with skin/cape data, server join handling, and blocked server listing. Integrates with auth and session repositories and includes error handling for various session operations. --- services/sessionService.js | 162 +++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 services/sessionService.js diff --git a/services/sessionService.js b/services/sessionService.js new file mode 100644 index 0000000..4845cc3 --- /dev/null +++ b/services/sessionService.js @@ -0,0 +1,162 @@ +const utils = require("../modules/utils") +const authRepository = require("../repositories/authRepository") +const sessionRepository = require("../repositories/sessionRepository") +const { DefaultError } = require("../errors/errors") + +async function registerLegacySession({ uuid, sessionId }) { + try { + await sessionRepository.insertLegacyClientSessions(sessionId, uuid) + return { code: 200 } + } catch (error) { + if (error instanceof DefaultError) throw error + throw new DefaultError(500, "Internal Server Error", error.toString()) + } +} + +async function validateLegacySession({ name, sessionId }) { + let userResult + try { + userResult = await authRepository.getUser(name) + } catch (error) { + if (error.code === 404) { + throw error + } + throw error + } + + try { + await sessionRepository.validateLegacyClientSession(sessionId, userResult.user.uuid) + return { code: 200 } + } catch (error) { + if (error.code === 404) { + throw new DefaultError(403, "Invalid session.", "ForbiddenOperationException") + } + throw error + } +} + +async function getBlockedServers() { + try { + return await sessionRepository.getBlockedServers() + } catch (error) { + throw new DefaultError(500, "Unable to fetch blocked servers.", error.toString()) + } +} + +async function getProfile({ uuid, unsigned = false }) { + let userResult + try { + userResult = await authRepository.getUser(uuid, false) + } catch (error) { + if (error.code === 404) { + return { code: 204, message: "User not found" } + } + throw error + } + + const dbUser = userResult.user + const username = dbUser.username + const cleanUuid = dbUser.uuid.replace(/-/g, "") + + const [skinResult, capeResult, actionsResult] = await Promise.all([ + sessionRepository.getActiveSkin(dbUser.uuid).catch(() => ({ data: null })), + sessionRepository.getActiveCape(dbUser.uuid).catch(() => ({ data: null })), + sessionRepository.getProfileActionsList(dbUser.uuid).catch(() => ({ data: [] })) + ]) + + const activeSkin = skinResult.data + const activeCape = capeResult.data + const profileActions = actionsResult.data || [] + + const isSkinBanned = profileActions.includes("USING_BANNED_SKIN") + const hasValidSkin = activeSkin && !isSkinBanned + const hasValidCape = !!activeCape + + const skinNode = hasValidSkin ? { + url: activeSkin.url, + metadata: activeSkin.variant === "SLIM" ? { model: "slim" } : undefined + } : undefined + + const capeNode = hasValidCape ? { + url: activeCape.url + } : undefined + + const texturesObject = { + ...(skinNode && { SKIN: skinNode }), + ...(capeNode && { CAPE: capeNode }) + } + + const texturePayload = { + timestamp: Date.now(), + profileId: cleanUuid, + profileName: username, + signatureRequired: !unsigned, + textures: texturesObject + } + + const payloadJson = JSON.stringify(texturePayload) + const base64Value = Buffer.from(payloadJson).toString("base64") + + const signature = unsigned ? null : utils.signProfileData(base64Value) + + const propertyNode = { + name: "textures", + value: base64Value, + ...(signature && { signature: signature }) + } + + return { + code: 200, + data: { + id: cleanUuid, + name: username, + properties: [propertyNode], + profileActions: profileActions + } + } +} + +async function joinServer({ accessToken, selectedProfile, clientToken, serverId, ip }) { + try { + await authRepository.validateClientSession(accessToken, clientToken) + } catch (error) { + throw new DefaultError(403, "Invalid access token", "ForbiddenOperationException") + } + await sessionRepository.saveServerSession(selectedProfile, accessToken, serverId, ip) + return { code: 204 } +} + +async function hasJoinedServer({ username, serverId, ip }) { + let userResult + try { + userResult = await authRepository.getUser(username, false) + } catch (error) { + if (error.code === 404) return { code: 204, message: "User not found" } + throw error + } + + const { uuid } = userResult.user + const joinCheck = await sessionRepository.getServerSession(uuid, serverId) + + if (joinCheck.code !== 200 || !joinCheck.valid) { + return { code: 204, message: "Join verification failed" } + } + + if (ip && ip.trim() !== "" && joinCheck.ip !== ip) { + return { code: 204, message: "Invalid IP address" } + } + + return await getProfile({ + uuid: uuid, + unsigned: false + }) +} + +module.exports = { + getProfile, + joinServer, + hasJoinedServer, + getBlockedServers, + registerLegacySession, + validateLegacySession, +} \ No newline at end of file