diff --git a/app/src/app.d.ts b/app/src/app.d.ts index 2fd17f5..b3b3abb 100644 --- a/app/src/app.d.ts +++ b/app/src/app.d.ts @@ -1,9 +1,13 @@ +import { KVNamespace } from '@cloudflare/workers-types'; // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { interface Platform { - env: Env + env: { + GH_MEDIA: R2Bucket; + GH_SESSIONS: KVNamespace; + }; cf: CfProperties ctx: ExecutionContext } diff --git a/app/src/lib/server/session.ts b/app/src/lib/server/session.ts new file mode 100644 index 0000000..a51f537 --- /dev/null +++ b/app/src/lib/server/session.ts @@ -0,0 +1,79 @@ +import type { KVNamespace } from "@cloudflare/workers-types"; +import { encodeBase32, encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; + +import type { RequestEvent } from "@sveltejs/kit"; + +// in seconds +const EXPIRATION_TTL: number = 60 * 60 * 24 * 7; + +export async function validateSessionToken(token: string, sessions: KVNamespace): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: Session | null = await sessions.get(sessionId, { type: "json" }); + + if (!session) { + return null; + } + + if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { + await sessions.put(sessionId, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); + } + return session; +} + +export async function invalidateSession(sessionId: string, sessions: KVNamespace): Promise { + await sessions.delete(sessionId); +} + +export async function invalidateUserSessions(userId: number, sessions: KVNamespace): Promise { + const keys = await sessions.list({ prefix: `${userId}:` }); + for (const key of keys.keys) { + await sessions.delete(key.name); + } +} + +export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void { + event.cookies.set("session", token, { + httpOnly: true, + path: "/", + secure: import.meta.env.PROD, + sameSite: "lax", + expires: expiresAt + }); +} + +export function deleteSessionTokenCookie(event: RequestEvent): void { + event.cookies.set("session", "", { + httpOnly: true, + path: "/", + secure: import.meta.env.PROD, + sameSite: "lax", + maxAge: 0 + }); +} + +export function generateSessionToken(): string { + const tokenBytes = new Uint8Array(20); + crypto.getRandomValues(tokenBytes); + const token = encodeBase32(tokenBytes).toLowerCase(); + return token; +} + +export async function createSession(token: string, userId: number, sessions: KVNamespace): Promise { + const sessionId = `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`; + const session: Session = { + id: sessionId, + userId, + expiresAt: new Date(Date.now() + 1000 * EXPIRATION_TTL) + }; + await sessions.put(sessionId, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); + return session; +} + +export interface Session { + id: string; + expiresAt: Date; + userId: number; +} + +type SessionValidationResult = Session | null; diff --git a/app/src/routes/login/+page.server.ts b/app/src/routes/login/+page.server.ts new file mode 100644 index 0000000..0c3f862 --- /dev/null +++ b/app/src/routes/login/+page.server.ts @@ -0,0 +1,10 @@ +import { redirect } from "@sveltejs/kit"; + +import type { RequestEvent } from "./$types"; + +export async function load(event: RequestEvent) { + if (event.locals.session !== null && event.locals.user !== null) { + return redirect(302, "/"); + } + return {}; +} \ No newline at end of file