Build Phase 2 judge vertical slice

This commit is contained in:
2026-08-06 22:42:39 -06:00
parent e6329ecabb
commit ec274419c3
41 changed files with 2945 additions and 669 deletions

View File

@@ -0,0 +1,24 @@
import type { MiddlewareHandler } from "hono"
import { failure } from "../http"
import { getSessionUser, type AuthUser } from "./session"
export interface AppEnv {
Variables: {
user: AuthUser | null
}
}
export const optionalAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
c.set("user", await getSessionUser(c))
await next()
}
export const requireAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
const user = await getSessionUser(c)
if (!user) {
return failure(c, 401, "login-required", "Authentication required")
}
c.set("user", user)
await next()
}

View File

@@ -0,0 +1,49 @@
import { pbkdf2, timingSafeEqual } from "node:crypto"
import { promisify } from "node:util"
const pbkdf2Async = promisify(pbkdf2)
async function verifyDjangoPbkdf2(password: string, encoded: string) {
const [algorithm, iterationsText, salt, digestText] = encoded.split("$")
if (
algorithm !== "pbkdf2_sha256" ||
!iterationsText ||
!salt ||
!digestText
) {
return false
}
const iterations = Number(iterationsText)
const expected = Buffer.from(digestText, "base64")
if (!Number.isSafeInteger(iterations) || iterations <= 0 || expected.length === 0) {
return false
}
const actual = await pbkdf2Async(
password,
salt,
iterations,
expected.length,
"sha256",
)
return timingSafeEqual(actual, expected)
}
export async function verifyPassword(password: string, encoded: string) {
if (encoded.startsWith("pbkdf2_sha256$")) {
return {
valid: await verifyDjangoPbkdf2(password, encoded),
needsUpgrade: true,
}
}
if (encoded.startsWith("$argon2")) {
return {
valid: await Bun.password.verify(password, encoded),
needsUpgrade: false,
}
}
return { valid: false, needsUpgrade: false }
}

View File

@@ -0,0 +1,110 @@
import { randomBytes } from "node:crypto"
import { eq } from "drizzle-orm"
import type { Context } from "hono"
import { deleteCookie, getCookie, setCookie } from "hono/cookie"
import { config } from "../config"
import { db, schema } from "../db"
import { redis } from "../redis"
const SESSION_PREFIX = "session:"
interface StoredSession {
userId: number
createdAt: string
}
export interface AuthUser {
id: number
username: string
email: string | null
adminType: string
problemPermission: string
isDisabled: boolean
}
function sessionKey(token: string) {
return `${SESSION_PREFIX}${token}`
}
export async function createSession(c: Context, userId: number) {
const token = randomBytes(32).toString("base64url")
const value: StoredSession = {
userId,
createdAt: new Date().toISOString(),
}
await redis.set(
sessionKey(token),
JSON.stringify(value),
"EX",
config.sessionTtlSeconds,
)
setCookie(c, config.sessionCookie, token, {
httpOnly: true,
sameSite: "Lax",
secure: config.secureCookies,
path: "/",
maxAge: config.sessionTtlSeconds,
})
}
export async function destroySession(c: Context) {
const token = getCookie(c, config.sessionCookie)
if (token) await redis.del(sessionKey(token))
deleteCookie(c, config.sessionCookie, { path: "/" })
}
function readCookie(request: Request, name: string) {
const header = request.headers.get("cookie")
if (!header) return undefined
for (const part of header.split(";")) {
const [key, ...value] = part.trim().split("=")
if (key === name) return decodeURIComponent(value.join("="))
}
return undefined
}
async function getUserByToken(token: string | undefined): Promise<AuthUser | null> {
if (!token) return null
const raw = await redis.get(sessionKey(token))
if (!raw) return null
let session: StoredSession
try {
session = JSON.parse(raw) as StoredSession
} catch {
await redis.del(sessionKey(token))
return null
}
const [user] = await db
.select({
id: schema.user.id,
username: schema.user.username,
email: schema.user.email,
adminType: schema.user.adminType,
problemPermission: schema.user.problemPermission,
isDisabled: schema.user.isDisabled,
})
.from(schema.user)
.where(eq(schema.user.id, session.userId))
.limit(1)
if (!user || user.isDisabled) {
await redis.del(sessionKey(token))
return null
}
await redis.expire(sessionKey(token), config.sessionTtlSeconds)
return user
}
export function getSessionUser(c: Context) {
return getUserByToken(getCookie(c, config.sessionCookie))
}
export function getRequestSessionUser(request: Request) {
return getUserByToken(readCookie(request, config.sessionCookie))
}