From d7a950d0e7a69319ac601ae3f72f25071bbcf022 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 16 Dec 2025 11:47:20 +0100 Subject: [PATCH] Device management --- assets/openapi.json | Bin 862487 -> 868215 bytes assets/schemas.json | Bin 385068 -> 388659 bytes src/api/routes/auth/sessions.ts | 75 +++++++++++++++++++++++ src/schemas/api/users/SessionsSchemas.ts | 55 +++++++++++++++++ src/util/entities/Session.ts | 20 +++++- 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/auth/sessions.ts create mode 100644 src/schemas/api/users/SessionsSchemas.ts diff --git a/assets/openapi.json b/assets/openapi.json index 6a5298b806111e074e636a8a4cb1065d10805888..31f9559c57c2091c8a362f88b8c36fee3e19f6b5 100644 GIT binary patch delta 828 zcmZ8gO=uHA6lQm3ck{EWNvqf-HHmH7)PgjHfWK&=NH4WUQyTD+L=u`{FXCNF|If-NNS@Qq;z(NtVZLE#act^1Wr4T+~_#xy$2bopwK}Ymc5p3sh7xQ^H`2_j&)xT|5b(lzB zsU5s;(Hj)}g%CokjCMSXJ?x-(gEK=hjNNSeEZoS3R2>dR zXY-5B(sz?l736cA9|8+VXckJ8*v`7tH9>-M1Y`K~l5c0k%dfc}!y>i8?tiZN2cli~ z=qs#!Ms7Hv_k&hs16!Nq)&uJ+1QgE2p3%Pa>Q*nLMsWw!BqX!k?mMvn&atg_mR)?K zoiz*Jzm34S_jC;VSUy`51H<1EVeD*@G_(i2m#`PqcYLaK$4nE^Y#Kfsv+ifzHpaOg z!7NWHonUgZzvs_Feg+L=jS2}Ce#k*my(&X!1Bvq$B$8FnUZUb52wdk+5!K#yO{^da zOd(K+6cUAr!c1XV5-Zl+9me*2pXnpiZkv8vhnhmwppcX6iXM&1R_mLfa)q~YfS41AxwbER&8;S)fT$9Hi3Q*8H*xMd|%_pQFv-P8!QUfw54l{(B^QK#LYE;*zpOe=wRi;98r59JQ>KGNzh z$1^o0%rERJ%73_uiRW|+=}XjxwRYAaK97e*j{9L;px!-ZI{hFus^2rMj6Pz;(ch&s zZQjDSKz{TWDCj~W>+>S8$Olm<&{2sLyE~m-!Ru%I7`(G|WGA|AlLzaaN>HRlaI(aX zyOgTn<1Dpp5tL?Bw=@M>PK`jj&CfX}4@wjK44&9@NJ?MW6p_IPo2reh)dFFB-75vf z->ezY7M?&uh5gq~;3BDLzRs(%+!8-2v<_92SI$o@%cLGemg$7d5nW*+e5){*8^C5| zPS7LZORF*U+T>qAR^sIV delta 29 jcmdmdM|{lz@rD-07N#xCC%xJ)L@)y}%k~QqtjSXW#V!oM diff --git a/src/api/routes/auth/sessions.ts b/src/api/routes/auth/sessions.ts new file mode 100644 index 000000000..2c28353bb --- /dev/null +++ b/src/api/routes/auth/sessions.ts @@ -0,0 +1,75 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +import { route } from "@spacebar/api"; +import { createHash } from "node:crypto"; +import { Session, Snowflake } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { SessionsLogoutSchema } from "../../../schemas/api/users/SessionsSchemas"; +import { In } from "typeorm"; +const router = Router({ mergeParams: true }); +router.get( + "/", + route({ + responses: { + 200: { + body: "GetSessionsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { extended = false } = req.params; + const sessions = (await Session.find({ where: { user_id: req.user_id, is_admin_session: false } })) as Session[]; + + res.json({ + user_sessions: sessions.map((session) => (extended ? session.getExtendedDeviceInfo() : session.getDiscordDeviceInfo())), + }); + }, +); + +router.post( + "/logout", + route({ + requestBody: "SessionsLogoutSchema", + responses: { + 204: {}, + }, + }), + async (req: Request, res: Response) => { + const body = req.body as SessionsLogoutSchema; + + let sessions: Session[] = []; + if ("session_ids" in body) { + sessions = (await Session.find({ where: { user_id: req.user_id, session_id: In(body.session_ids!) } })) as Session[]; + } + + if ("session_id_hashes" in body) { + const allSessions = (await Session.find({ where: { user_id: req.user_id } })) as Session[]; + const hashSet = new Set(body.session_id_hashes); + const matchingSessions = allSessions.filter((session) => { + const hash = createHash("sha256").update(session.session_id).digest("hex"); + return hashSet.has(hash); + }); + sessions.push(...matchingSessions); + } + + for (const session of sessions) { + await session.remove(); + } + }, +); +export default router; diff --git a/src/schemas/api/users/SessionsSchemas.ts b/src/schemas/api/users/SessionsSchemas.ts new file mode 100644 index 000000000..35dee7c6c --- /dev/null +++ b/src/schemas/api/users/SessionsSchemas.ts @@ -0,0 +1,55 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { ActivitySchema, Snowflake } from "@spacebar/schemas"; +import { ClientStatus } from "@spacebar/util"; + +export type SessionsLogoutSchema = { session_ids?: Snowflake[]; session_id_hashes?: string[] }; +export type GetSessionsResponse = { user_sessions: DeviceInfo[]; }; +/*return { + id: this.session_id, + id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), + status: this.status, + activities: this.activities, + client_status: this.client_status, + approx_last_used_time: this.last_seen.toISOString(), + client_info: { + ...this.client_info, + location: this.last_seen_location, + }, + last_seen: this.last_seen, + last_seen_ip: this.last_seen_ip, + last_seen_location: this.last_seen_location, + };*/ +export type DeviceInfo = { + id_hash: string; + approx_last_used_time: string; + client_info: { + client: string; + os: string; + version: number; + location: string; + }; + id?: string; + status?: string; + activities?: ActivitySchema["activities"][]; + client_status?: ClientStatus; + last_seen?: Date; + last_seen_ip?: string; + last_seen_location?: string; +}; \ No newline at end of file diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts index 1569543f3..4ff9e10c5 100644 --- a/src/util/entities/Session.ts +++ b/src/util/entities/Session.ts @@ -87,10 +87,28 @@ export class Session extends BaseClassWithoutId { client_info: { os: this.client_info.os, client: this.client_info.client, - location: this.last_seen_location + location: this.last_seen_location, }, }; } + + getExtendedDeviceInfo() { + return { + id: this.session_id, + id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), + status: this.status, + activities: this.activities, + client_status: this.client_status, + approx_last_used_time: this.last_seen.toISOString(), + client_info: { + ...this.client_info, + location: this.last_seen_location, + }, + last_seen: this.last_seen, + last_seen_ip: this.last_seen_ip, + last_seen_location: this.last_seen_location, + }; + } } export const PrivateSessionProjection: (keyof Session)[] = ["user_id", "session_id", "activities", "client_info", "status"];