2024-11-16 22:41:43 -05:00
|
|
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
|
|
|
import { sha256 } from "@oslojs/crypto/sha2";
|
2024-11-17 22:44:11 -05:00
|
|
|
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
2024-11-16 22:41:43 -05:00
|
|
|
import db from "@server/db";
|
|
|
|
import { eq, and } from "drizzle-orm";
|
2024-11-27 00:07:40 -05:00
|
|
|
import config from "@server/config";
|
2024-11-16 22:41:43 -05:00
|
|
|
|
|
|
|
export const SESSION_COOKIE_NAME = "resource_session";
|
|
|
|
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
2024-11-27 00:07:40 -05:00
|
|
|
export const SECURE_COOKIES = config.server.secure_cookies;
|
|
|
|
export const COOKIE_DOMAIN =
|
|
|
|
"." + new URL(config.app.base_url).hostname.split(".").slice(-2).join(".");
|
2024-11-16 22:41:43 -05:00
|
|
|
|
2024-11-17 22:44:11 -05:00
|
|
|
export async function createResourceSession(opts: {
|
|
|
|
token: string;
|
|
|
|
resourceId: number;
|
|
|
|
passwordId?: number;
|
|
|
|
pincodeId?: number;
|
2024-12-15 17:47:07 -05:00
|
|
|
whitelistId: number;
|
|
|
|
usedOtp?: boolean;
|
2024-11-17 22:44:11 -05:00
|
|
|
}): Promise<ResourceSession> {
|
|
|
|
if (!opts.passwordId && !opts.pincodeId) {
|
|
|
|
throw new Error(
|
2024-12-15 17:47:07 -05:00
|
|
|
"At least one of passwordId or pincodeId must be provided"
|
2024-11-17 22:44:11 -05:00
|
|
|
);
|
|
|
|
}
|
2024-11-16 22:41:43 -05:00
|
|
|
|
|
|
|
const sessionId = encodeHexLowerCase(
|
2024-12-15 17:47:07 -05:00
|
|
|
sha256(new TextEncoder().encode(opts.token))
|
2024-11-16 22:41:43 -05:00
|
|
|
);
|
2024-11-17 22:44:11 -05:00
|
|
|
|
2024-11-16 22:41:43 -05:00
|
|
|
const session: ResourceSession = {
|
|
|
|
sessionId: sessionId,
|
|
|
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
2024-11-17 22:44:11 -05:00
|
|
|
resourceId: opts.resourceId,
|
|
|
|
passwordId: opts.passwordId || null,
|
|
|
|
pincodeId: opts.pincodeId || null,
|
2024-12-15 17:47:07 -05:00
|
|
|
whitelistId: opts.whitelistId,
|
|
|
|
usedOtp: opts.usedOtp || false
|
2024-11-16 22:41:43 -05:00
|
|
|
};
|
2024-11-17 22:44:11 -05:00
|
|
|
|
2024-11-16 22:41:43 -05:00
|
|
|
await db.insert(resourceSessions).values(session);
|
2024-11-17 22:44:11 -05:00
|
|
|
|
2024-11-16 22:41:43 -05:00
|
|
|
return session;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function validateResourceSessionToken(
|
2024-11-17 22:44:11 -05:00
|
|
|
token: string,
|
2024-12-15 17:47:07 -05:00
|
|
|
resourceId: number
|
2024-11-16 22:41:43 -05:00
|
|
|
): Promise<ResourceSessionValidationResult> {
|
|
|
|
const sessionId = encodeHexLowerCase(
|
2024-12-15 17:47:07 -05:00
|
|
|
sha256(new TextEncoder().encode(token))
|
2024-11-16 22:41:43 -05:00
|
|
|
);
|
|
|
|
const result = await db
|
2024-11-17 22:44:11 -05:00
|
|
|
.select()
|
2024-11-16 22:41:43 -05:00
|
|
|
.from(resourceSessions)
|
2024-11-17 22:44:11 -05:00
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(resourceSessions.sessionId, sessionId),
|
2024-12-15 17:47:07 -05:00
|
|
|
eq(resourceSessions.resourceId, resourceId)
|
|
|
|
)
|
2024-11-17 22:44:11 -05:00
|
|
|
);
|
|
|
|
|
2024-11-16 22:41:43 -05:00
|
|
|
if (result.length < 1) {
|
2024-11-17 22:44:11 -05:00
|
|
|
return { resourceSession: null };
|
2024-11-16 22:41:43 -05:00
|
|
|
}
|
2024-11-17 22:44:11 -05:00
|
|
|
|
|
|
|
const resourceSession = result[0];
|
|
|
|
|
2024-11-16 22:41:43 -05:00
|
|
|
if (Date.now() >= resourceSession.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
|
|
|
resourceSession.expiresAt = new Date(
|
2024-12-15 17:47:07 -05:00
|
|
|
Date.now() + SESSION_COOKIE_EXPIRES
|
2024-11-16 22:41:43 -05:00
|
|
|
).getTime();
|
|
|
|
await db
|
|
|
|
.update(resourceSessions)
|
|
|
|
.set({
|
2024-12-15 17:47:07 -05:00
|
|
|
expiresAt: resourceSession.expiresAt
|
2024-11-16 22:41:43 -05:00
|
|
|
})
|
|
|
|
.where(eq(resourceSessions.sessionId, resourceSession.sessionId));
|
|
|
|
}
|
2024-11-17 22:44:11 -05:00
|
|
|
|
|
|
|
return { resourceSession };
|
2024-11-16 22:41:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function invalidateResourceSession(
|
2024-12-15 17:47:07 -05:00
|
|
|
sessionId: string
|
2024-11-16 22:41:43 -05:00
|
|
|
): Promise<void> {
|
|
|
|
await db
|
|
|
|
.delete(resourceSessions)
|
|
|
|
.where(eq(resourceSessions.sessionId, sessionId));
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function invalidateAllSessions(
|
2024-11-17 22:44:11 -05:00
|
|
|
resourceId: number,
|
|
|
|
method?: {
|
|
|
|
passwordId?: number;
|
|
|
|
pincodeId?: number;
|
2024-12-15 17:47:07 -05:00
|
|
|
whitelistId?: number;
|
|
|
|
}
|
2024-11-16 22:41:43 -05:00
|
|
|
): Promise<void> {
|
2024-11-17 22:44:11 -05:00
|
|
|
if (method?.passwordId) {
|
2024-11-16 22:41:43 -05:00
|
|
|
await db
|
|
|
|
.delete(resourceSessions)
|
2024-11-17 22:44:11 -05:00
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(resourceSessions.resourceId, resourceId),
|
2024-12-15 17:47:07 -05:00
|
|
|
eq(resourceSessions.passwordId, method.passwordId)
|
|
|
|
)
|
2024-11-17 22:44:11 -05:00
|
|
|
);
|
2024-12-15 17:47:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (method?.pincodeId) {
|
2024-11-16 22:41:43 -05:00
|
|
|
await db
|
|
|
|
.delete(resourceSessions)
|
|
|
|
.where(
|
|
|
|
and(
|
2024-11-17 22:44:11 -05:00
|
|
|
eq(resourceSessions.resourceId, resourceId),
|
2024-12-15 17:47:07 -05:00
|
|
|
eq(resourceSessions.pincodeId, method.pincodeId)
|
|
|
|
)
|
2024-11-16 22:41:43 -05:00
|
|
|
);
|
2024-12-15 17:47:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (method?.whitelistId) {
|
|
|
|
await db
|
|
|
|
.delete(resourceSessions)
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(resourceSessions.resourceId, resourceId),
|
|
|
|
eq(resourceSessions.whitelistId, method.whitelistId)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
if (!method?.passwordId && !method?.pincodeId && !method?.whitelistId) {
|
2024-11-17 22:44:11 -05:00
|
|
|
await db
|
|
|
|
.delete(resourceSessions)
|
|
|
|
.where(eq(resourceSessions.resourceId, resourceId));
|
2024-11-16 22:41:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:44:11 -05:00
|
|
|
export function serializeResourceSessionCookie(
|
2024-11-27 00:07:40 -05:00
|
|
|
cookieName: string,
|
2024-11-16 22:41:43 -05:00
|
|
|
token: string,
|
2024-12-15 17:47:07 -05:00
|
|
|
fqdn: string
|
2024-11-16 22:41:43 -05:00
|
|
|
): string {
|
2024-11-27 00:07:40 -05:00
|
|
|
if (SECURE_COOKIES) {
|
|
|
|
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
2024-11-16 22:41:43 -05:00
|
|
|
} else {
|
2024-11-27 00:07:40 -05:00
|
|
|
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
2024-11-16 22:41:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:44:11 -05:00
|
|
|
export function createBlankResourceSessionTokenCookie(
|
2024-11-27 00:07:40 -05:00
|
|
|
cookieName: string,
|
2024-12-15 17:47:07 -05:00
|
|
|
fqdn: string
|
2024-11-16 22:41:43 -05:00
|
|
|
): string {
|
2024-11-27 00:07:40 -05:00
|
|
|
if (SECURE_COOKIES) {
|
|
|
|
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
2024-11-16 22:41:43 -05:00
|
|
|
} else {
|
2024-11-27 00:07:40 -05:00
|
|
|
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
2024-11-16 22:41:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:44:11 -05:00
|
|
|
export type ResourceSessionValidationResult = {
|
|
|
|
resourceSession: ResourceSession | null;
|
|
|
|
};
|