mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-13 05:40:38 +01:00
168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import {
|
|
encodeBase32LowerCaseNoPadding,
|
|
encodeHexLowerCase
|
|
} from "@oslojs/encoding";
|
|
import { sha256 } from "@oslojs/crypto/sha2";
|
|
import {
|
|
resourceSessions,
|
|
Session,
|
|
sessions,
|
|
User,
|
|
users
|
|
} from "@server/db/schemas";
|
|
import db from "@server/db";
|
|
import { eq, inArray } from "drizzle-orm";
|
|
import config from "@server/lib/config";
|
|
import type { RandomReader } from "@oslojs/crypto/random";
|
|
import { generateRandomString } from "@oslojs/crypto/random";
|
|
import logger from "@server/logger";
|
|
|
|
export const SESSION_COOKIE_NAME =
|
|
config.getRawConfig().server.session_cookie_name;
|
|
export const SESSION_COOKIE_EXPIRES =
|
|
1000 *
|
|
60 *
|
|
60 *
|
|
config.getRawConfig().server.dashboard_session_length_hours;
|
|
export const COOKIE_DOMAIN =
|
|
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
|
|
|
export function generateSessionToken(): string {
|
|
const bytes = new Uint8Array(20);
|
|
crypto.getRandomValues(bytes);
|
|
const token = encodeBase32LowerCaseNoPadding(bytes);
|
|
return token;
|
|
}
|
|
|
|
export async function createSession(
|
|
token: string,
|
|
userId: string
|
|
): Promise<Session> {
|
|
const sessionId = encodeHexLowerCase(
|
|
sha256(new TextEncoder().encode(token))
|
|
);
|
|
const session: Session = {
|
|
sessionId: sessionId,
|
|
userId,
|
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
|
};
|
|
await db.insert(sessions).values(session);
|
|
return session;
|
|
}
|
|
|
|
export async function validateSessionToken(
|
|
token: string
|
|
): Promise<SessionValidationResult> {
|
|
const sessionId = encodeHexLowerCase(
|
|
sha256(new TextEncoder().encode(token))
|
|
);
|
|
const result = await db
|
|
.select({ user: users, session: sessions })
|
|
.from(sessions)
|
|
.innerJoin(users, eq(sessions.userId, users.userId))
|
|
.where(eq(sessions.sessionId, sessionId));
|
|
if (result.length < 1) {
|
|
return { session: null, user: null };
|
|
}
|
|
const { user, session } = result[0];
|
|
if (Date.now() >= session.expiresAt) {
|
|
await db
|
|
.delete(sessions)
|
|
.where(eq(sessions.sessionId, session.sessionId));
|
|
return { session: null, user: null };
|
|
}
|
|
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
|
session.expiresAt = new Date(
|
|
Date.now() + SESSION_COOKIE_EXPIRES
|
|
).getTime();
|
|
await db.transaction(async (trx) => {
|
|
await trx
|
|
.update(sessions)
|
|
.set({
|
|
expiresAt: session.expiresAt
|
|
})
|
|
.where(eq(sessions.sessionId, session.sessionId));
|
|
|
|
await trx
|
|
.update(resourceSessions)
|
|
.set({
|
|
expiresAt: session.expiresAt
|
|
})
|
|
.where(eq(resourceSessions.userSessionId, session.sessionId));
|
|
});
|
|
}
|
|
return { session, user };
|
|
}
|
|
|
|
export async function invalidateSession(sessionId: string): Promise<void> {
|
|
try {
|
|
await db.transaction(async (trx) => {
|
|
await trx
|
|
.delete(resourceSessions)
|
|
.where(eq(resourceSessions.userSessionId, sessionId));
|
|
await trx.delete(sessions).where(eq(sessions.sessionId, sessionId));
|
|
});
|
|
} catch (e) {
|
|
logger.error("Failed to invalidate session", e);
|
|
}
|
|
}
|
|
|
|
export async function invalidateAllSessions(userId: string): Promise<void> {
|
|
try {
|
|
await db.transaction(async (trx) => {
|
|
const userSessions = await trx
|
|
.select()
|
|
.from(sessions)
|
|
.where(eq(sessions.userId, userId));
|
|
await trx.delete(resourceSessions).where(
|
|
inArray(
|
|
resourceSessions.userSessionId,
|
|
userSessions.map((s) => s.sessionId)
|
|
)
|
|
);
|
|
await trx.delete(sessions).where(eq(sessions.userId, userId));
|
|
});
|
|
} catch (e) {
|
|
logger.error("Failed to all invalidate user sessions", e);
|
|
}
|
|
}
|
|
|
|
export function serializeSessionCookie(
|
|
token: string,
|
|
isSecure: boolean,
|
|
expiresAt: Date
|
|
): string {
|
|
if (isSecure) {
|
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
} else {
|
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/;`;
|
|
}
|
|
}
|
|
|
|
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
|
if (isSecure) {
|
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
} else {
|
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
|
}
|
|
}
|
|
|
|
const random: RandomReader = {
|
|
read(bytes: Uint8Array): void {
|
|
crypto.getRandomValues(bytes);
|
|
}
|
|
};
|
|
|
|
export function generateId(length: number): string {
|
|
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
return generateRandomString(random, alphabet, length);
|
|
}
|
|
|
|
export function generateIdFromEntropySize(size: number): string {
|
|
const buffer = crypto.getRandomValues(new Uint8Array(size));
|
|
return encodeBase32LowerCaseNoPadding(buffer);
|
|
}
|
|
|
|
export type SessionValidationResult =
|
|
| { session: Session; user: User }
|
|
| { session: null; user: null };
|