- Add me.get procedure returning user profile with needsSetup flag - Add me.setupProfile procedure for initial profile setup after signup - Add nonEmptyString/optionalString schema helpers with tests - Use Web Crypto API (SubtleCrypto) for Cloudflare Workers compatibility - Use @formatjs/intl-durationformat for duration formatting - Remove node:crypto dependency from crypto utilities Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
76 lines
1.9 KiB
TypeScript
76 lines
1.9 KiB
TypeScript
/**
|
|
* Authentication utilities for token handling
|
|
*/
|
|
|
|
import type { Database } from "@reviq/db-schema";
|
|
import type { Kysely } from "kysely";
|
|
import { hashToken } from "./crypto.js";
|
|
|
|
export interface AuthenticatedUser {
|
|
id: number;
|
|
email: string;
|
|
isSuperuser: boolean;
|
|
}
|
|
|
|
/**
|
|
* Authenticate a request using session token or API key
|
|
* Returns the authenticated user or null if not authenticated
|
|
*/
|
|
export const authenticateRequest = async (
|
|
db: Kysely<Database>,
|
|
sessionToken?: string,
|
|
apiKey?: string,
|
|
): Promise<AuthenticatedUser | null> => {
|
|
// Try session cookie first, then API key
|
|
const token = sessionToken ?? apiKey;
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
|
|
const tokenHash = await hashToken(token);
|
|
|
|
// Check sessions table
|
|
const session = await db
|
|
.selectFrom("sessions")
|
|
.innerJoin("users", "users.id", "sessions.user_id")
|
|
.where("sessions.token_hash", "=", tokenHash)
|
|
.where("sessions.expires_at", ">", new Date())
|
|
.where("sessions.revoked_at", "is", null)
|
|
.select(["users.id", "users.email", "users.is_superuser"])
|
|
.executeTakeFirst();
|
|
|
|
if (session) {
|
|
return {
|
|
id: session.id,
|
|
email: session.email,
|
|
isSuperuser: session.is_superuser,
|
|
};
|
|
}
|
|
|
|
// Check API tokens table
|
|
const apiToken = await db
|
|
.selectFrom("api_tokens")
|
|
.innerJoin("users", "users.id", "api_tokens.user_id")
|
|
.where("api_tokens.token_hash", "=", tokenHash)
|
|
.where("api_tokens.expires_at", ">", new Date())
|
|
.select(["users.id", "users.email", "users.is_superuser"])
|
|
.executeTakeFirst();
|
|
|
|
if (apiToken) {
|
|
// Update last_used_at
|
|
await db
|
|
.updateTable("api_tokens")
|
|
.set({ last_used_at: new Date() })
|
|
.where("token_hash", "=", tokenHash)
|
|
.execute();
|
|
|
|
return {
|
|
id: apiToken.id,
|
|
email: apiToken.email,
|
|
isSuperuser: apiToken.is_superuser,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
};
|