mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-01 16:45:40 +02:00
refactor and reorganize
This commit is contained in:
parent
9732098799
commit
3b4a993704
216 changed files with 519 additions and 2128 deletions
118
server/auth/sessions/app.ts
Normal file
118
server/auth/sessions/app.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
encodeBase32LowerCaseNoPadding,
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Session, sessions, User, users } from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import type { RandomReader } from "@oslojs/crypto/random";
|
||||
import { generateRandomString } from "@oslojs/crypto/random";
|
||||
|
||||
export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||
|
||||
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
|
||||
.update(sessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
})
|
||||
.where(eq(sessions.sessionId, session.sessionId));
|
||||
}
|
||||
return { session, user };
|
||||
}
|
||||
|
||||
export async function invalidateSession(sessionId: string): Promise<void> {
|
||||
await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||
}
|
||||
|
||||
export function serializeSessionCookie(token: string): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankSessionTokenCookie(): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
72
server/auth/sessions/newt.ts
Normal file
72
server/auth/sessions/newt.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Newt, newts, newtSessions, NewtSession } from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
export async function createNewtSession(
|
||||
token: string,
|
||||
newtId: string,
|
||||
): Promise<NewtSession> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
);
|
||||
const session: NewtSession = {
|
||||
sessionId: sessionId,
|
||||
newtId,
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
||||
};
|
||||
await db.insert(newtSessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateNewtSessionToken(
|
||||
token: string,
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
);
|
||||
const result = await db
|
||||
.select({ newt: newts, session: newtSessions })
|
||||
.from(newtSessions)
|
||||
.innerJoin(newts, eq(newtSessions.newtId, newts.newtId))
|
||||
.where(eq(newtSessions.sessionId, sessionId));
|
||||
if (result.length < 1) {
|
||||
return { session: null, newt: null };
|
||||
}
|
||||
const { newt, session } = result[0];
|
||||
if (Date.now() >= session.expiresAt) {
|
||||
await db
|
||||
.delete(newtSessions)
|
||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||
return { session: null, newt: null };
|
||||
}
|
||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
||||
session.expiresAt = new Date(
|
||||
Date.now() + EXPIRES,
|
||||
).getTime();
|
||||
await db
|
||||
.update(newtSessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
})
|
||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||
}
|
||||
return { session, newt };
|
||||
}
|
||||
|
||||
export async function invalidateNewtSession(sessionId: string): Promise<void> {
|
||||
await db.delete(newtSessions).where(eq(newtSessions.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export async function invalidateAllNewtSessions(newtId: string): Promise<void> {
|
||||
await db.delete(newtSessions).where(eq(newtSessions.newtId, newtId));
|
||||
}
|
||||
|
||||
export type SessionValidationResult =
|
||||
| { session: NewtSession; newt: Newt }
|
||||
| { session: null; newt: null };
|
186
server/auth/sessions/resource.ts
Normal file
186
server/auth/sessions/resource.ts
Normal file
|
@ -0,0 +1,186 @@
|
|||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.resource_session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||
|
||||
export async function createResourceSession(opts: {
|
||||
token: string;
|
||||
resourceId: number;
|
||||
passwordId?: number;
|
||||
pincodeId?: number;
|
||||
whitelistId?: number;
|
||||
accessTokenId?: string;
|
||||
usedOtp?: boolean;
|
||||
doNotExtend?: boolean;
|
||||
expiresAt?: number | null;
|
||||
sessionLength?: number | null;
|
||||
}): Promise<ResourceSession> {
|
||||
if (
|
||||
!opts.passwordId &&
|
||||
!opts.pincodeId &&
|
||||
!opts.whitelistId &&
|
||||
!opts.accessTokenId
|
||||
) {
|
||||
throw new Error("Auth method must be provided");
|
||||
}
|
||||
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(opts.token))
|
||||
);
|
||||
|
||||
const session: ResourceSession = {
|
||||
sessionId: sessionId,
|
||||
expiresAt:
|
||||
opts.expiresAt ||
|
||||
new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
sessionLength: opts.sessionLength || SESSION_COOKIE_EXPIRES,
|
||||
resourceId: opts.resourceId,
|
||||
passwordId: opts.passwordId || null,
|
||||
pincodeId: opts.pincodeId || null,
|
||||
whitelistId: opts.whitelistId || null,
|
||||
doNotExtend: opts.doNotExtend || false,
|
||||
accessTokenId: opts.accessTokenId || null
|
||||
};
|
||||
|
||||
await db.insert(resourceSessions).values(session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateResourceSessionToken(
|
||||
token: string,
|
||||
resourceId: number
|
||||
): Promise<ResourceSessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const result = await db
|
||||
.select()
|
||||
.from(resourceSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceSessions.sessionId, sessionId),
|
||||
eq(resourceSessions.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
|
||||
if (result.length < 1) {
|
||||
return { resourceSession: null };
|
||||
}
|
||||
|
||||
const resourceSession = result[0];
|
||||
|
||||
if (Date.now() >= resourceSession.expiresAt) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.sessionId, resourceSessions.sessionId));
|
||||
return { resourceSession: null };
|
||||
} else if (
|
||||
Date.now() >=
|
||||
resourceSession.expiresAt - resourceSession.sessionLength / 2
|
||||
) {
|
||||
if (!resourceSession.doNotExtend) {
|
||||
resourceSession.expiresAt = new Date(
|
||||
Date.now() + resourceSession.sessionLength
|
||||
).getTime();
|
||||
await db
|
||||
.update(resourceSessions)
|
||||
.set({
|
||||
expiresAt: resourceSession.expiresAt
|
||||
})
|
||||
.where(
|
||||
eq(resourceSessions.sessionId, resourceSession.sessionId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { resourceSession };
|
||||
}
|
||||
|
||||
export async function invalidateResourceSession(
|
||||
sessionId: string
|
||||
): Promise<void> {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export async function invalidateAllSessions(
|
||||
resourceId: number,
|
||||
method?: {
|
||||
passwordId?: number;
|
||||
pincodeId?: number;
|
||||
whitelistId?: number;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (method?.passwordId) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceSessions.resourceId, resourceId),
|
||||
eq(resourceSessions.passwordId, method.passwordId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (method?.pincodeId) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceSessions.resourceId, resourceId),
|
||||
eq(resourceSessions.pincodeId, method.pincodeId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.resourceId, resourceId));
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeResourceSessionCookie(
|
||||
cookieName: string,
|
||||
token: string
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankResourceSessionTokenCookie(
|
||||
cookieName: string
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
}
|
||||
|
||||
export type ResourceSessionValidationResult = {
|
||||
resourceSession: ResourceSession | null;
|
||||
};
|
9
server/auth/sessions/verifySession.ts
Normal file
9
server/auth/sessions/verifySession.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Request } from "express";
|
||||
import { validateSessionToken, SESSION_COOKIE_NAME } from "@server/auth/sessions/app";
|
||||
|
||||
export async function verifySession(req: Request) {
|
||||
const res = await validateSessionToken(
|
||||
req.cookies[SESSION_COOKIE_NAME] ?? "",
|
||||
);
|
||||
return res;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue