atums.world/src/helpers/sessions.ts

194 lines
4.9 KiB
TypeScript

import { jwt } from "@config/environment";
import { environment } from "@config/environment";
import { redis } from "@helpers/redis";
import { createDecoder, createSigner, createVerifier } from "fast-jwt";
type Signer = (payload: UserSession, options?: UserSession) => string;
type Verifier = (token: string, options?: UserSession) => UserSession;
type Decoder = (token: string, options?: UserSession) => UserSession;
class SessionManager {
private signer: Signer;
private verifier: Verifier;
private decoder: Decoder;
constructor() {
this.signer = createSigner({
key: jwt.secret,
expiresIn: jwt.expiresIn,
});
this.verifier = createVerifier({ key: jwt.secret });
this.decoder = createDecoder();
}
public async createSession(
payload: UserSession,
userAgent: string,
): Promise<string> {
const token: string = this.signer(payload);
const sessionKey: string = `session:${payload.id}:${token}`;
await redis
.getInstance()
.set(
"JSON",
sessionKey,
{ ...payload, userAgent },
this.getExpirationInSeconds(),
);
const cookie: string = this.generateCookie(token);
return cookie;
}
public async getSession(request: Request): Promise<UserSession | null> {
const cookie: string | null = request.headers.get("Cookie");
if (!cookie) return null;
const token: string | null =
cookie.match(/session=([^;]+)/)?.[1] || null;
if (!token) return null;
const userSessions: string[] = await redis
.getInstance()
.keys("session:*:" + token);
if (!userSessions.length) return null;
const sessionData: unknown = await redis
.getInstance()
.get("JSON", userSessions[0]);
if (!sessionData) return null;
const payload: UserSession & { userAgent: string } =
sessionData as UserSession & { userAgent: string };
return payload;
}
public async updateSession(
request: Request,
payload: UserSession,
userAgent: string,
): Promise<string> {
const cookie: string | null = request.headers.get("Cookie");
if (!cookie) throw new Error("No session found in request");
const token: string | null =
cookie.match(/session=([^;]+)/)?.[1] || null;
if (!token) throw new Error("Session token not found");
const userSessions: string[] = await redis
.getInstance()
.keys("session:*:" + token);
if (!userSessions.length)
throw new Error("Session not found or expired");
const sessionKey: string = userSessions[0];
await redis
.getInstance()
.set(
"JSON",
sessionKey,
{ ...payload, userAgent },
this.getExpirationInSeconds(),
);
return this.generateCookie(token);
}
public async verifySession(token: string): Promise<UserSession> {
const userSessions: string[] = await redis
.getInstance()
.keys("session:*:" + token);
if (!userSessions.length)
throw new Error("Session not found or expired");
const sessionData: unknown = await redis
.getInstance()
.get("JSON", userSessions[0]);
if (!sessionData) throw new Error("Session not found or expired");
const payload: UserSession = this.verifier(token);
return payload;
}
public async decodeSession(token: string): Promise<UserSession> {
const payload: UserSession = this.decoder(token);
return payload;
}
public async invalidateSession(request: Request): Promise<void> {
const cookie: string | null = request.headers.get("Cookie");
if (!cookie) return;
const token: string | null =
cookie.match(/session=([^;]+)/)?.[1] || null;
if (!token) return;
const userSessions: string[] = await redis
.getInstance()
.keys("session:*:" + token);
if (!userSessions.length) return;
await redis.getInstance().delete("JSON", userSessions[0]);
}
private generateCookie(
token: string,
maxAge: number = this.getExpirationInSeconds(),
options?: {
secure?: boolean;
httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None";
path?: string;
domain?: string;
},
): string {
const {
secure = !environment.development,
httpOnly = true,
sameSite = environment.development ? "Lax" : "None",
path = "/",
domain,
} = options || {};
let cookie: string = `session=${encodeURIComponent(token)}; Path=${path}; Max-Age=${maxAge}`;
if (httpOnly) cookie += "; HttpOnly";
if (secure) cookie += "; Secure";
if (sameSite) cookie += `; SameSite=${sameSite}`;
if (domain) cookie += `; Domain=${domain}`;
return cookie;
}
private getExpirationInSeconds(): number {
const match: RegExpMatchArray | null =
jwt.expiresIn.match(/^(\d+)([smhd])$/);
if (!match) {
throw new Error("Invalid expiresIn format in jwt config");
}
const [, value, unit] = match;
const num: number = parseInt(value, 10);
switch (unit) {
case "s":
return num;
case "m":
return num * 60;
case "h":
return num * 3600;
case "d":
return num * 86400;
default:
throw new Error("Invalid time unit in expiresIn");
}
}
}
const sessionManager: SessionManager = new SessionManager();
export { sessionManager };