forked from atums.world/backend
start of the 50th remake of a file host doomed to never be finished
This commit is contained in:
commit
46c05ca3a9
33 changed files with 2155 additions and 0 deletions
162
src/helpers/sessions.ts
Normal file
162
src/helpers/sessions.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
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 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 };
|
Loading…
Add table
Add a link
Reference in a new issue