checkpoint

This commit is contained in:
Michael Freno
2026-01-21 12:22:19 -05:00
parent 1d8ec7a375
commit 58d48dac70
29 changed files with 287 additions and 2594 deletions

View File

@@ -1,8 +1,136 @@
import type { H3Event } from "vinxi/http";
import { getCookie, setCookie } from "vinxi/http";
import { OAuth2Client } from "google-auth-library";
import type { Row } from "@libsql/client/web";
import { SignJWT, jwtVerify } from "jose";
import { env } from "~/env/server";
import { getAuthSession } from "./session-helpers";
import { ConnectionFactory } from "./database";
import { AUTH_CONFIG, expiryToSeconds, getAccessTokenExpiry } from "~/config";
export const authCookieName = "auth_token";
type AuthTokenPayload = {
sub: string;
email: string | null;
isAdmin: boolean;
iat?: number;
exp?: number;
};
function getAuthCookieOptions(rememberMe: boolean) {
return {
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax" as const,
path: "/",
maxAge: rememberMe
? expiryToSeconds(AUTH_CONFIG.ACCESS_TOKEN_EXPIRY_LONG)
: undefined
};
}
function getAuthHeaderToken(event: H3Event): string | null {
const requestHeader = event.request?.headers?.get?.("authorization") || null;
const eventHeader = event.headers
? typeof (event.headers as any).get === "function"
? (event.headers as any).get("authorization")
: (event.headers as any).authorization
: null;
const nodeHeader = event.node?.req?.headers?.authorization || null;
const header = requestHeader || eventHeader || nodeHeader || null;
if (!header) return null;
const normalized = header.trim();
if (!normalized.toLowerCase().startsWith("bearer ")) return null;
return normalized.slice("Bearer ".length).trim();
}
export function getAuthTokenFromEvent(event: H3Event): string | null {
return getCookie(event, authCookieName) || getAuthHeaderToken(event);
}
export async function verifyAuthToken(
token: string
): Promise<AuthTokenPayload | null> {
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
if (!payload.sub) {
return null;
}
return {
sub: payload.sub as string,
email: (payload.email as string | null) ?? null,
isAdmin: (payload.isAdmin as boolean) ?? false,
iat: payload.iat,
exp: payload.exp
};
} catch (error) {
console.error("Auth token verification failed:", error);
return null;
}
}
export async function getAuthPayloadFromEvent(
event: H3Event
): Promise<AuthTokenPayload | null> {
const token = getAuthTokenFromEvent(event);
if (!token) return null;
return verifyAuthToken(token);
}
export async function issueAuthToken({
event,
userId,
rememberMe
}: {
event: H3Event;
userId: string;
rememberMe: boolean;
}): Promise<string> {
const conn = ConnectionFactory();
const result = await conn.execute({
sql: "SELECT email, is_admin FROM User WHERE id = ?",
args: [userId]
});
if (result.rows.length === 0) {
throw new Error("User not found");
}
const row = result.rows[0] as { email?: string | null; is_admin?: number };
const isAdmin = row.is_admin === 1;
const email = row.email ?? null;
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const expiry = rememberMe
? AUTH_CONFIG.ACCESS_TOKEN_EXPIRY_LONG
: getAccessTokenExpiry();
const token = await new SignJWT({ email, isAdmin })
.setProtectedHeader({ alg: "HS256" })
.setSubject(userId)
.setIssuedAt()
.setExpirationTime(expiry)
.sign(secret);
setCookie(event, authCookieName, token, getAuthCookieOptions(rememberMe));
return token;
}
export function clearAuthToken(event: H3Event): void {
setCookie(event, authCookieName, "", {
...getAuthCookieOptions(true),
maxAge: 0
});
setCookie(event, "csrf-token", "", {
httpOnly: false,
secure: env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 0
});
}
/**
* Check authentication status
@@ -15,9 +143,8 @@ export async function checkAuthStatus(event: H3Event): Promise<{
isAdmin: boolean;
}> {
try {
const session = await getAuthSession(event);
if (!session || !session.userId) {
const payload = await getAuthPayloadFromEvent(event);
if (!payload) {
return {
isAuthenticated: false,
userId: null,
@@ -27,8 +154,8 @@ export async function checkAuthStatus(event: H3Event): Promise<{
return {
isAuthenticated: true,
userId: session.userId,
isAdmin: session.isAdmin
userId: payload.sub,
isAdmin: payload.isAdmin
};
} catch (error) {
console.error("Auth check error:", error);
@@ -41,7 +168,7 @@ export async function checkAuthStatus(event: H3Event): Promise<{
}
/**
* Get user ID from session
* Get user ID from auth token
* @param event - H3Event
* @returns User ID or null if not authenticated
*/
@@ -67,10 +194,8 @@ export async function validateLineageRequest({
const { provider, email } = userRow;
if (provider === "email") {
try {
const { jwtVerify } = await import("jose");
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(auth_token, secret);
if (email !== payload.email) {
const payload = await verifyAuthToken(auth_token);
if (!payload || email !== payload.email) {
return false;
}
} catch (err) {