From b175a08dce61a89c4a362dd8fbfa7f2c3e1b9945 Mon Sep 17 00:00:00 2001 From: Bas950 Date: Wed, 11 Sep 2024 18:37:30 +0200 Subject: [PATCH] feat: add session-keep-alive --- .../src/functions/clearOldSessions.ts | 37 +++++------ apps/api-master/src/index.ts | 4 +- apps/api-worker/environment.d.ts | 1 + apps/api-worker/src/classes/Socket.ts | 40 ++++++------ apps/api-worker/src/functions/createServer.ts | 14 +++-- .../resolvers/v5/Mutation/addScience.ts | 2 +- .../graphql/resolvers/v5/Mutation/index.ts | 2 +- .../src/graphql/resolvers/v5/Query/index.ts | 2 +- .../graphql/resolvers/v5/Query/presences.ts | 2 +- .../src/graphql/resolvers/v5/index.ts | 4 +- .../src/graphql/schema/v5/addScience.gql | 6 +- apps/api-worker/src/index.ts | 1 + .../api-worker/src/routes/sessionKeepAlive.ts | 59 ++++++++++++++++++ apps/api-worker/src/tracing.ts | 2 +- apps/pd/src/routes/createFromBase64.ts | 2 +- apps/pd/src/routes/createFromImage.test.ts | 6 +- apps/pd/src/routes/createFromImage.ts | 2 +- apps/pd/src/routes/createShortenedLink.ts | 2 +- apps/pd/src/routes/getFullLink.test.ts | 4 +- apps/pd/src/routes/getFullLink.ts | 4 +- apps/schema-server/src/index.ts | 2 +- apps/website/pages/store/index.vue | 61 ++++++++++++++----- apps/website/plugins/fontawesome.ts | 2 +- apps/website/server/api/getStaffData.ts | 13 ++-- apps/website/stores/useExtension.ts | 1 + 25 files changed, 187 insertions(+), 88 deletions(-) create mode 100644 apps/api-worker/src/routes/sessionKeepAlive.ts diff --git a/apps/api-master/src/functions/clearOldSessions.ts b/apps/api-master/src/functions/clearOldSessions.ts index 6a865b9..641baa8 100644 --- a/apps/api-master/src/functions/clearOldSessions.ts +++ b/apps/api-master/src/functions/clearOldSessions.ts @@ -1,36 +1,38 @@ import { REST } from "@discordjs/rest"; import { mainLog, redis } from "../index.js"; -export async function clearOldSesssions() { - const sessions = await redis.hgetall("pmd-api.sessions"); - const now = Date.now(); +export async function clearOldSessions() { + const sessionKeys = await redis.keys("pmd:session:*"); + const now = Math.floor(Date.now() / 1000); - if (Object.keys(sessions).length === 0) { + if (sessionKeys.length === 0) { mainLog("No sessions to clear"); return; } - mainLog(`Checking ${Object.keys(sessions).length} sessions`); + mainLog(`Checking ${sessionKeys.length} sessions`); let cleared = 0; - for (const [key, value] of Object.entries(sessions)) { - const session = JSON.parse(value) as { - token: string; - session: string; - lastUpdated: number; - }; + for (const key of sessionKeys) { + const session = await redis.hgetall(key); - // ? If the session is younger than 30seconds, skip it - if (now - session.lastUpdated < 30000) + if (!session.t || !session.u) { + await redis.del(key); + cleared++; + continue; + } + + //* If the session is younger than 30 seconds, skip it + if (now - Number(session.u) < 30) continue; - // ? Delete the session + //* Delete the session try { const discord = new REST({ version: "10", authPrefix: "Bearer" }); - discord.setToken(session.token); + discord.setToken(session.t); await discord.post("/users/@me/headless-sessions/delete", { body: { - token: session.session, + token: key.split(":")[2], // Extract session token from key }, }); } @@ -39,8 +41,7 @@ export async function clearOldSesssions() { } cleared++; - - await redis.hdel("pmd-api.sessions", key); + await redis.del(key); } mainLog(`Cleared ${cleared} sessions`); diff --git a/apps/api-master/src/index.ts b/apps/api-master/src/index.ts index 091775a..554a0fc 100644 --- a/apps/api-master/src/index.ts +++ b/apps/api-master/src/index.ts @@ -1,8 +1,8 @@ import { CronJob } from "cron"; import debug from "debug"; +import { clearOldSessions } from "./functions/clearOldSessions.js"; import createRedis from "./functions/createRedis.js"; -import { clearOldSesssions } from "./functions/clearOldSessions.js"; export const redis = createRedis(); @@ -14,7 +14,7 @@ void new CronJob( // Every 5 seconds "*/5 * * * * *", async () => { - clearOldSesssions(); + clearOldSessions(); }, undefined, true, diff --git a/apps/api-worker/environment.d.ts b/apps/api-worker/environment.d.ts index 0dfcbc3..f2cfae7 100644 --- a/apps/api-worker/environment.d.ts +++ b/apps/api-worker/environment.d.ts @@ -2,5 +2,6 @@ declare namespace NodeJS { export interface ProcessEnv { NODE_ENV?: "development" | "production" | "test"; DATABASE_URL?: string; + SESSION_KEEP_ALIVE_INTERVAL?: string; } } diff --git a/apps/api-worker/src/classes/Socket.ts b/apps/api-worker/src/classes/Socket.ts index 97d1dea..1b4367e 100644 --- a/apps/api-worker/src/classes/Socket.ts +++ b/apps/api-worker/src/classes/Socket.ts @@ -1,9 +1,9 @@ -import { scope, type } from "arktype"; -import type { FastifyRequest } from "fastify"; -import WebSocket from "ws"; -import type { RawData } from "ws"; import { REST } from "@discordjs/rest"; +import { scope, type } from "arktype"; import { Routes } from "discord-api-types/v10"; +import WebSocket from "ws"; +import type { FastifyRequest } from "fastify"; +import type { RawData } from "ws"; import { redis } from "../functions/createServer.js"; import { counter } from "../tracing.js"; @@ -11,20 +11,20 @@ const schema = scope({ token: { "+": "delete", "type": "'token'", - "token": "format.trim", - "expires": "unixTimestamp", + "token": "string.trim", + "expires": "number.epoch", }, session: { "+": "delete", "type": "'session'", - "token": "format.trim", + "token": "string.trim", }, validMessages: "token | session", }).export(); export class Socket { currentToken: typeof schema.token.infer | undefined; - currentSesssion: typeof schema.session.infer | undefined; + currentSession: typeof schema.session.infer | undefined; discord = new REST({ version: "10", authPrefix: "Bearer" }); constructor( @@ -54,8 +54,8 @@ export class Socket { break; } case "session": { - await redis.hdel("pmd-api.sessions", out.token); - this.currentSesssion = out; + await redis.del(`pmd:session:${out.token}`); + this.currentSession = out; break; } } @@ -69,18 +69,20 @@ export class Socket { async onClose() { counter.add(-1); - if (!this.currentToken || !this.currentSesssion) + if (!this.currentToken || !this.currentSession) return; - await redis.hset( - "pmd-api.sessions", - this.currentSesssion.token, - JSON.stringify({ - session: this.currentSesssion.token, - token: this.currentToken.token, - lastUpdated: Date.now(), - }), + const now = Math.floor(Date.now() / 1000); + + await redis.hmset( + `pmd:session:${this.currentSession.token}`, + { + t: this.currentToken.token, + u: now, + }, ); + + await redis.expire(`pmd:session:${this.currentSession.token}`, 60); // Expire after 1 minute } async isTokenValid(token: typeof schema.token.infer) { diff --git a/apps/api-worker/src/functions/createServer.ts b/apps/api-worker/src/functions/createServer.ts index abca9b2..6d11ab9 100644 --- a/apps/api-worker/src/functions/createServer.ts +++ b/apps/api-worker/src/functions/createServer.ts @@ -6,14 +6,15 @@ import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases"; import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; import { maxDirectivesPlugin } from "@escape.tech/graphql-armor-max-directives"; import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; -import type { FastifyReply, FastifyRequest } from "fastify"; -import fastify from "fastify"; -import { createSchema, createYoga } from "graphql-yoga"; - import fastifyWebsocket from "@fastify/websocket"; import { defu } from "defu"; -import { resolvers } from "../graphql/resolvers/v5/index.js"; +import fastify from "fastify"; + +import { createSchema, createYoga } from "graphql-yoga"; +import type { FastifyReply, FastifyRequest } from "fastify"; import { Socket } from "../classes/Socket.js"; +import { resolvers } from "../graphql/resolvers/v5/index.js"; +import { sessionKeepAlive } from "../routes/sessionKeepAlive.js"; import createRedis from "./createRedis.js"; export interface FastifyContext { @@ -91,11 +92,14 @@ export default async function createServer() { const test = defu(flags, { WebSocketManager: true, + SessionKeepAlive: true, }); void reply.send(test); }); + app.post("/v5/session-keep-alive", sessionKeepAlive); + return app; } diff --git a/apps/api-worker/src/graphql/resolvers/v5/Mutation/addScience.ts b/apps/api-worker/src/graphql/resolvers/v5/Mutation/addScience.ts index 40486e9..45004aa 100644 --- a/apps/api-worker/src/graphql/resolvers/v5/Mutation/addScience.ts +++ b/apps/api-worker/src/graphql/resolvers/v5/Mutation/addScience.ts @@ -1,6 +1,6 @@ import { type } from "arktype"; -import type { MutationResolvers } from "../../../../generated/graphql-v5.js"; import { redis } from "../../../../functions/createServer.js"; +import type { MutationResolvers } from "../../../../generated/graphql-v5.js"; const addScienceSchema = type({ identifier: "uuid & format.lowercase", diff --git a/apps/api-worker/src/graphql/resolvers/v5/Mutation/index.ts b/apps/api-worker/src/graphql/resolvers/v5/Mutation/index.ts index 4d66715..66b7e6b 100644 --- a/apps/api-worker/src/graphql/resolvers/v5/Mutation/index.ts +++ b/apps/api-worker/src/graphql/resolvers/v5/Mutation/index.ts @@ -1,6 +1,6 @@ -import type { MutationResolvers } from "../../../../generated/graphql-v5.js"; import addScience from "./addScience.js"; import heartbeat from "./heartbeat.js"; +import type { MutationResolvers } from "../../../../generated/graphql-v5.js"; export const Mutation: MutationResolvers = { addScience, diff --git a/apps/api-worker/src/graphql/resolvers/v5/Query/index.ts b/apps/api-worker/src/graphql/resolvers/v5/Query/index.ts index e86623b..23cc27a 100644 --- a/apps/api-worker/src/graphql/resolvers/v5/Query/index.ts +++ b/apps/api-worker/src/graphql/resolvers/v5/Query/index.ts @@ -1,5 +1,5 @@ -import type { QueryResolvers } from "../../../../generated/graphql-v5.js"; import presences from "./presences.js"; +import type { QueryResolvers } from "../../../../generated/graphql-v5.js"; export const Query: QueryResolvers = { presences, diff --git a/apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts b/apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts index 3e00990..c48f59f 100644 --- a/apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts +++ b/apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts @@ -1,6 +1,6 @@ import { Presence } from "@premid/db"; -import type { PresenceSchema } from "@premid/db/Presence.js"; import { parseResolveInfo } from "graphql-parse-resolve-info"; +import type { PresenceSchema } from "@premid/db/Presence.js"; import type { FilterQuery } from "mongoose"; import type { QueryResolvers } from "../../../../generated/graphql-v5.js"; diff --git a/apps/api-worker/src/graphql/resolvers/v5/index.ts b/apps/api-worker/src/graphql/resolvers/v5/index.ts index e172abd..1f3d4b3 100644 --- a/apps/api-worker/src/graphql/resolvers/v5/index.ts +++ b/apps/api-worker/src/graphql/resolvers/v5/index.ts @@ -1,6 +1,6 @@ -import type { Resolvers } from "../../../generated/graphql-v5.js"; -import { Query } from "./Query/index.js"; import { Mutation } from "./Mutation/index.js"; +import { Query } from "./Query/index.js"; +import type { Resolvers } from "../../../generated/graphql-v5.js"; export const resolvers: Resolvers = { Query, diff --git a/apps/api-worker/src/graphql/schema/v5/addScience.gql b/apps/api-worker/src/graphql/schema/v5/addScience.gql index 0765acc..a62fd92 100644 --- a/apps/api-worker/src/graphql/schema/v5/addScience.gql +++ b/apps/api-worker/src/graphql/schema/v5/addScience.gql @@ -1,9 +1,5 @@ type Mutation { - addScience( - identifier: String! - presences: [String!]! - platform: PlatformInput! - ): AddScienceResult + addScience(identifier: String!, presences: [String!]!, platform: PlatformInput!): AddScienceResult } input PlatformInput { diff --git a/apps/api-worker/src/index.ts b/apps/api-worker/src/index.ts index 707669e..1a557f0 100644 --- a/apps/api-worker/src/index.ts +++ b/apps/api-worker/src/index.ts @@ -4,6 +4,7 @@ import * as Sentry from "@sentry/node"; import { connect } from "mongoose"; import "./tracing.js"; +// eslint-disable-next-line perfectionist/sort-imports import createServer from "./functions/createServer.js"; // TODO SETUP SENTRY diff --git a/apps/api-worker/src/routes/sessionKeepAlive.ts b/apps/api-worker/src/routes/sessionKeepAlive.ts new file mode 100644 index 0000000..6e408bb --- /dev/null +++ b/apps/api-worker/src/routes/sessionKeepAlive.ts @@ -0,0 +1,59 @@ +import process from "node:process"; +import { REST } from "@discordjs/rest"; +import { type } from "arktype"; +import { Routes } from "discord-api-types/v10"; +import type { FastifyReply, FastifyRequest } from "fastify"; +import { redis } from "../functions/createServer.js"; + +const schema = type({ + token: "string.trim", + session: "string.trim", +}); + +export async function sessionKeepAlive(request: FastifyRequest, reply: FastifyReply) { + //* Get the 2 headers + const out = schema({ + token: request.headers["x-token"], + session: request.headers["x-session"], + }); + + if (out instanceof type.errors) + return reply.status(400).send({ code: "MISSING_HEADERS", message: out.message }); + + if (!await isTokenValid(out.token)) + return reply.status(400).send({ code: "INVALID_TOKEN", message: "The token is invalid" }); + + const now = Math.floor(Date.now() / 1000); // Unix timestamp in seconds + + await redis.hmset( + `pmd:session:${out.session}`, + { + t: out.token, + u: now, + }, + ); + + // Set expiration for the hash + await redis.expire(`pmd:session:${out.session}`, 60); // Expire after 1 minute + + const interval = Number.parseInt(process.env.SESSION_KEEP_ALIVE_INTERVAL ?? "5000"); + + return reply.status(200).send({ + code: "OK", + message: "Session updated", + nextUpdate: interval, + }); +} + +async function isTokenValid(token: string) { + const discord = new REST({ version: "10", authPrefix: "Bearer" }); + + discord.setToken(token); + try { + await discord.get(Routes.user()); + return true; + } + catch { + return false; + } +} diff --git a/apps/api-worker/src/tracing.ts b/apps/api-worker/src/tracing.ts index 056e707..652c128 100644 --- a/apps/api-worker/src/tracing.ts +++ b/apps/api-worker/src/tracing.ts @@ -1,6 +1,6 @@ +import { ValueType } from "@opentelemetry/api"; import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; -import { ValueType } from "@opentelemetry/api"; const prometheusExporter = new PrometheusExporter(); diff --git a/apps/pd/src/routes/createFromBase64.ts b/apps/pd/src/routes/createFromBase64.ts index ac3950d..c908583 100644 --- a/apps/pd/src/routes/createFromBase64.ts +++ b/apps/pd/src/routes/createFromBase64.ts @@ -1,9 +1,9 @@ import crypto from "node:crypto"; import process from "node:process"; -import type { RouteHandlerMethod } from "fastify"; import mime from "mime-types"; import { nanoid } from "nanoid"; +import type { RouteHandlerMethod } from "fastify"; import keyv from "../keyv.js"; diff --git a/apps/pd/src/routes/createFromImage.test.ts b/apps/pd/src/routes/createFromImage.test.ts index d159483..22c290c 100644 --- a/apps/pd/src/routes/createFromImage.test.ts +++ b/apps/pd/src/routes/createFromImage.test.ts @@ -1,10 +1,10 @@ +import { Buffer } from "node:buffer"; import { readFile } from "node:fs/promises"; +import { afterAll, beforeAll, describe, it } from "vitest"; + import type { RequestOptions } from "node:http"; import type { AddressInfo } from "node:net"; -import { Buffer } from "node:buffer"; -import { afterAll, beforeAll, describe, it } from "vitest"; - import { createServer } from "../functions/createServer.js"; describe.concurrent("createFromImage", async () => { diff --git a/apps/pd/src/routes/createFromImage.ts b/apps/pd/src/routes/createFromImage.ts index 73e1cd8..7b8f9f9 100644 --- a/apps/pd/src/routes/createFromImage.ts +++ b/apps/pd/src/routes/createFromImage.ts @@ -1,9 +1,9 @@ import crypto from "node:crypto"; import process from "node:process"; -import type { RouteHandlerMethod } from "fastify"; import { fileTypeFromBuffer } from "file-type"; import { nanoid } from "nanoid"; +import type { RouteHandlerMethod } from "fastify"; import keyv from "../keyv.js"; diff --git a/apps/pd/src/routes/createShortenedLink.ts b/apps/pd/src/routes/createShortenedLink.ts index 9f0dac4..9e8f555 100644 --- a/apps/pd/src/routes/createShortenedLink.ts +++ b/apps/pd/src/routes/createShortenedLink.ts @@ -1,8 +1,8 @@ import crypto from "node:crypto"; import process from "node:process"; -import type { RouteHandlerMethod } from "fastify"; import { nanoid } from "nanoid"; +import type { RouteHandlerMethod } from "fastify"; import keyv from "../keyv.js"; diff --git a/apps/pd/src/routes/getFullLink.test.ts b/apps/pd/src/routes/getFullLink.test.ts index 2e6cf91..7e0d31a 100644 --- a/apps/pd/src/routes/getFullLink.test.ts +++ b/apps/pd/src/routes/getFullLink.test.ts @@ -1,6 +1,6 @@ -import { readFile } from "node:fs/promises"; - import { Buffer } from "node:buffer"; + +import { readFile } from "node:fs/promises"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { createServer } from "../functions/createServer.js"; diff --git a/apps/pd/src/routes/getFullLink.ts b/apps/pd/src/routes/getFullLink.ts index b85c320..2844af6 100644 --- a/apps/pd/src/routes/getFullLink.ts +++ b/apps/pd/src/routes/getFullLink.ts @@ -1,6 +1,6 @@ -import crypto from "node:crypto"; - import { Buffer } from "node:buffer"; + +import crypto from "node:crypto"; import type { RouteHandlerMethod } from "fastify"; import isInCIDRRange from "../functions/isInCidRange.js"; diff --git a/apps/schema-server/src/index.ts b/apps/schema-server/src/index.ts index d11d358..7c1a958 100644 --- a/apps/schema-server/src/index.ts +++ b/apps/schema-server/src/index.ts @@ -2,9 +2,9 @@ import { extname, resolve } from "node:path"; import process from "node:process"; import helmet from "@fastify/helmet"; -import type { RequestGenericInterface } from "fastify"; import fastify from "fastify"; import { globby } from "globby"; +import type { RequestGenericInterface } from "fastify"; export const app = fastify(); diff --git a/apps/website/pages/store/index.vue b/apps/website/pages/store/index.vue index 3d03859..adc1762 100644 --- a/apps/website/pages/store/index.vue +++ b/apps/website/pages/store/index.vue @@ -1,7 +1,7 @@