From 405338c314c6c79217d925da33da8dec089c97ce Mon Sep 17 00:00:00 2001 From: Akhil Reddy Date: Fri, 24 Jan 2025 19:24:27 +0530 Subject: [PATCH] add cache to auth --- .gitignore | 3 +- package.json | 5 +++- pnpm-lock.yaml | 17 +++++++++++ src/auth.ts | 48 +++++++++++++----------------- src/auth/auth.route.ts | 7 ++--- src/auth/auth.service.ts | 29 ++++++++++++++++-- src/user/user.schema.ts | 64 +++++++++++++++++++++------------------- src/utils/cache.ts | 19 ++++++++++++ 8 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 src/utils/cache.ts diff --git a/.gitignore b/.gitignore index 8f00ef2..403afef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env -dist \ No newline at end of file +dist +test \ No newline at end of file diff --git a/package.json b/package.json index b2a3503..5f529c2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "dist/index.js", "scripts": { "build": "tsc -p tsconfig.json", - "start": "tsc -w & node --watch --env-file=.env dist/index.js" + "start": "tsc -w & node --watch --env-file=.env dist/index.js", + "load_test": "export $(cat .env | xargs) && k6 run ./test/loadTest.js" }, "keywords": [], "author": "", @@ -22,12 +23,14 @@ "fastify": "^5.2.0", "fastify-type-provider-zod": "^4.0.2", "fastify-zod": "^1.4.0", + "lru-cache": "^11.0.2", "mongoose": "^8.9.0", "zod": "^3.24.1" }, "devDependencies": { "@types/bcrypt": "^5.0.2", "@types/node": "^22.10.2", + "k6": "^0.0.0", "typescript": "^5.7.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fd889b..ce766d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: fastify-zod: specifier: ^1.4.0 version: 1.4.0(fastify@5.2.0) + lru-cache: + specifier: ^11.0.2 + version: 11.0.2 mongoose: specifier: ^8.9.0 version: 8.9.0 @@ -54,6 +57,9 @@ importers: '@types/node': specifier: ^22.10.2 version: 22.10.2 + k6: + specifier: ^0.0.0 + version: 0.0.0 typescript: specifier: ^5.7.2 version: 5.7.2 @@ -790,6 +796,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + k6@0.0.0: + resolution: {integrity: sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g==} + kareem@2.6.3: resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} engines: {node: '>=12.0.0'} @@ -800,6 +809,10 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -2474,6 +2487,8 @@ snapshots: json-schema-traverse@1.0.0: {} + k6@0.0.0: {} + kareem@2.6.3: {} light-my-request@6.3.0: @@ -2486,6 +2501,8 @@ snapshots: dependencies: tslib: 2.8.1 + lru-cache@11.0.2: {} + make-dir@3.1.0: dependencies: semver: 6.3.1 diff --git a/src/auth.ts b/src/auth.ts index 1266230..047dbb8 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,8 +3,13 @@ import { FastifyReply, FastifyRequest } from "fastify"; import { getToken } from "./tokens/token.service"; import { Claim } from "./utils/claims"; import { OAuth2Namespace } from "@fastify/oauth2"; -import { getSession } from "./auth/auth.service"; -import { roles, rules } from "./utils/roles"; +import { deleteSession, getSession } from "./auth/auth.service"; +import { rules } from "./utils/roles"; +import { + cacheSession, + getCachedSession, + removeCachedSession, +} from "./utils/cache"; export type AuthenticatedUser = { sid?: string; @@ -14,6 +19,7 @@ export type AuthenticatedUser = { role?: string; tenantId: string; claims: Array; + expiry?: string; }; declare module "fastify" { @@ -56,35 +62,21 @@ export async function authHandler(req: FastifyRequest, res: FastifyReply) { claims: tokenInDb.claims as Array, }; } else { - const sessionInDb = await getSession(authHeader); - if (sessionInDb === null) + let session = getCachedSession(authHeader); + if (!session) { + session = await getSession(authHeader); + cacheSession(authHeader, session); + } + + if (!session) return res.code(401).send({ error: "invalid_token" }); + + if (new Date() > new Date(session.expiry)) { + removeCachedSession(authHeader); + await deleteSession(authHeader); return res.code(401).send({ error: "invalid_token" }); - - if (new Date() > new Date(sessionInDb.expiresAt)) { - await sessionInDb.deleteOne(); - return res.code(401).send({ error: "session_expired" }); } - //@ts-ignore - if (!rules[sessionInDb.user.role]) { - return res.code(401).send({ error: "no role" }); - } - - req.user = { - sid: authHeader, - //@ts-ignore - type: sessionInDb.user.type, - //@ts-ignore - userId: sessionInDb.user.id, - //@ts-ignore - tenantId: sessionInDb.user.tenantId, - //@ts-ignore - orgId: sessionInDb.user.orgId, - //@ts-ignore - role: sessionInDb.user.role, - //@ts-ignore - claims: rules[sessionInDb.user.role].claims, - }; + req.user = session; } } diff --git a/src/auth/auth.route.ts b/src/auth/auth.route.ts index c542322..0ad0f0d 100644 --- a/src/auth/auth.route.ts +++ b/src/auth/auth.route.ts @@ -1,6 +1,6 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { getUserByEmail, updateUser } from "../user/user.service"; -import { createSession, getSession } from "./auth.service"; +import { createSession, deleteSession, getSession } from "./auth.service"; export async function authRoutes(fastify: FastifyInstance) { fastify.get( @@ -55,10 +55,7 @@ export async function authRoutes(fastify: FastifyInstance) { if (!req.headers.authorization) return res.code(200).send(); const auth = req.headers.authorization.split(" ")[1]; - const sessionInDb = await getSession(auth); - if (sessionInDb === null) return res.code(200).send(); - - await sessionInDb.deleteOne(); + await deleteSession(auth); return res.code(200).send(); }); } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index c712e70..1e34f24 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,4 +1,7 @@ +import { AuthenticatedUser } from "../auth"; +import { userModel } from "../user/user.schema"; import { generateToken } from "../utils/id"; +import { rules } from "../utils/roles"; import { sessionModel } from "./auth.schema"; export async function createSession(userId: string, ip?: string, ua?: string) { @@ -19,6 +22,28 @@ export async function createSession(userId: string, ip?: string, ua?: string) { return newSession; } -export async function getSession(sessionId: string) { - return await sessionModel.findOne({ sid: sessionId }).populate("user"); +export async function getSession( + sessionId: string +): Promise { + console.log("DB HIT"); + const session = await sessionModel.findOne({ sid: sessionId }); + if (session === null) return null; + + const user = await userModel.findById(session.user); + if (user === null) return null; + + return { + sid: session.id, + type: "user", + userId: user.id, + orgId: user.orgId ? user.orgId.toString() : null, + role: user.role, + tenantId: user.tenantId, + claims: rules[user.role].claims ?? [], + expiry: new Date(session.expiresAt).toISOString(), + }; +} + +export async function deleteSession(sessionId: string) { + return await sessionModel.deleteOne({ sid: sessionId }); } diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index d2b96b0..9ec2ed5 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -1,37 +1,41 @@ import { buildJsonSchemas } from "fastify-zod"; -import mongoose from "mongoose"; +import mongoose, { InferSchemaType } from "mongoose"; import { z } from "zod"; import { roles } from "../utils/roles"; -export const userModel = mongoose.model( - "user", - new mongoose.Schema({ - tenantId: { - type: String, - required: true, - }, - pid: { - type: String, - unique: true, - required: true, - }, - orgId: mongoose.Types.ObjectId, - firstName: String, - lastName: String, - name: String, - email: { - type: String, - unique: true, - required: true, - }, - avatar: String, - status: String, - role: String, - createdAt: Date, - createdBy: mongoose.Types.ObjectId, - lastLogin: Date, - }) -); +const userSchema = new mongoose.Schema({ + tenantId: { + type: String, + required: true, + }, + pid: { + type: String, + unique: true, + required: true, + }, + orgId: mongoose.Types.ObjectId, + firstName: String, + lastName: String, + name: String, + email: { + type: String, + unique: true, + required: true, + }, + avatar: String, + status: String, + role: { + type: String, + required: true, + }, + createdAt: Date, + createdBy: mongoose.Types.ObjectId, + lastLogin: Date, +}); + +export const userModel = mongoose.model("user", userSchema); + +export type User = InferSchemaType; const userCore = { firstName: z.string().max(30), diff --git a/src/utils/cache.ts b/src/utils/cache.ts new file mode 100644 index 0000000..000bc56 --- /dev/null +++ b/src/utils/cache.ts @@ -0,0 +1,19 @@ +import { LRUCache } from "lru-cache"; +import { AuthenticatedUser } from "../auth"; + +const sessionCache = new LRUCache({ + max: 100, + ttl: 1000 * 60 * 60, +}); + +export function cacheSession(key: string, user: AuthenticatedUser) { + sessionCache.set(key, user); +} + +export function getCachedSession(key: string) { + return sessionCache.get(key); +} + +export function removeCachedSession(key: string) { + sessionCache.delete(key); +}