Files
drop/server/internal/session/index.ts

126 lines
3.6 KiB
TypeScript

import type { H3Event } from "h3";
import type { Session, SessionProvider } from "./types";
import { randomUUID } from "node:crypto";
import { parse as parseCookies } from "cookie-es";
import type { MinimumRequestObject } from "~/server/h3";
import type { DurationLike } from "luxon";
import { DateTime } from "luxon";
import createCacheSessionProvider from "./cache";
/*
This implementation may need work.
It exposes an API that should stay static, but there are plenty of opportunities for optimisation/organisation under the hood
*/
const dropTokenCookieName = "drop-token";
const normalSessionLength: DurationLike = {
days: 31,
};
const extendedSessionLength: DurationLike = {
year: 1,
};
export class SessionHandler {
private sessionProvider: SessionProvider;
constructor() {
// Create a new provider
this.sessionProvider = createCacheSessionProvider();
// this.sessionProvider = createDBSessionHandler();
// this.sessionProvider = createMemorySessionProvider();
}
async signin(h3: H3Event, userId: string, rememberMe: boolean = false) {
const expiresAt = this.createExipreAt(rememberMe);
const token = this.createSessionCookie(h3, expiresAt);
return await this.sessionProvider.setSession(token, {
userId,
expiresAt,
data: {},
});
}
/**
* Get a session associated with a request
* @returns session
*/
async getSession<T extends Session>(request: MinimumRequestObject) {
const token = this.getSessionToken(request);
if (!token) return undefined;
// TODO: should validate if session is expired or not here, not in application code
const data = await this.sessionProvider.getSession<T>(token);
return data;
}
/**
* Signout session associated with request and deauthenticates it
* @param request
* @returns
*/
async signout(h3: H3Event) {
const token = this.getSessionToken(h3);
if (!token) return false;
const res = await this.sessionProvider.removeSession(token);
if (!res) return false;
deleteCookie(h3, dropTokenCookieName);
return true;
}
async cleanupSessions() {
await this.sessionProvider.cleanupSessions();
}
/**
* Update session info
* @param token session token
* @param data new session data
* @returns success or not
*/
private async updateSession(token: string, data: Session) {
return await this.sessionProvider.updateSession(token, data);
}
// ---------------------- Private API Below ------------------------
/**
* Get session token on a request
* @param request
* @returns session token
*/
private getSessionToken(
request: MinimumRequestObject | undefined,
): string | undefined {
if (!request) throw new Error("Native web request not available");
const cookieHeader = request.headers.get("Cookie");
if (!cookieHeader) return undefined;
const cookies = parseCookies(cookieHeader);
const cookie = cookies[dropTokenCookieName];
return cookie;
}
private createExipreAt(rememberMe: boolean) {
return DateTime.now()
.plus(rememberMe ? extendedSessionLength : normalSessionLength)
.toJSDate();
}
/**
* Creates cookie that represents user session
* @param h3
* @param extend
* @returns
*/
private createSessionCookie(h3: H3Event, expiresAt: Date) {
const token = randomUUID();
// TODO: we should probably switch to jwts to minimize possibility of someone
// trying to guess a session id (jwts let us sign + encrypt stuff in a std way)
setCookie(h3, dropTokenCookieName, token, { expires: expiresAt });
return token;
}
}
export const sessionHandler = new SessionHandler();
export default sessionHandler;