FastAPI and TypeScript improved

This commit is contained in:
2026-06-04 05:14:29 -04:00
parent 67ad91b673
commit 66b2db81fb
28 changed files with 1864 additions and 717 deletions

View File

@@ -8,6 +8,8 @@
import { getFunction, getContextGroups } from './registry'
import { resolveInvalidation, formatInvalidateHeader } from './invalidation'
import { getCache, cacheGet, cachePut, cachePurge } from './cache'
import { ANONYMOUS, type Identity } from './identity'
import type { AuthRequirement } from './types'
let _cacheSecret: string | null = null
@@ -22,6 +24,54 @@ export interface MizanResponse {
headers: Record<string, string>
}
interface AuthDenial {
status: 401 | 403
code: 'UNAUTHORIZED' | 'FORBIDDEN'
message: string
}
/**
* Check whether `identity` satisfies the stored `auth` requirement.
* Ports Django's _check_auth_requirement exactly. Returns an AuthDenial
* on failure, or null when access is allowed.
*/
function checkAuth(auth: AuthRequirement | undefined, identity: Identity): AuthDenial | null {
if (auth === undefined) return null
// Callable runs first — before the authentication gate.
if (typeof auth === 'function') {
try {
return auth(identity)
? null
: { status: 403, code: 'FORBIDDEN', message: 'Access denied' }
} catch (e: any) {
return { status: 403, code: 'FORBIDDEN', message: e?.message || 'Access denied' }
}
}
if (!identity.isAuthenticated) {
return { status: 401, code: 'UNAUTHORIZED', message: 'Authentication required' }
}
if (auth === 'staff' && !identity.isStaff) {
return { status: 403, code: 'FORBIDDEN', message: 'Staff access required' }
}
if (auth === 'superuser' && !identity.isSuperuser) {
return { status: 403, code: 'FORBIDDEN', message: 'Superuser access required' }
}
return null
}
function authDenialResponse(denial: AuthDenial): MizanResponse {
return {
status: denial.status,
body: { error: true, code: denial.code, message: denial.message },
headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
}
}
/**
* Handle GET /api/mizan/ctx/:contextName/
*
@@ -30,6 +80,7 @@ export interface MizanResponse {
export async function handleContextFetch(
contextName: string,
params: Record<string, string>,
identity: Identity = ANONYMOUS,
): Promise<MizanResponse> {
const groups = getContextGroups()
const fnNames = groups[contextName]
@@ -42,6 +93,15 @@ export async function handleContextFetch(
}
}
// Auth pre-pass — run BEFORE the cache lookup so a cache HIT can never
// leak to an unauthorized caller. Any denial short-circuits, uncached.
for (const fnName of fnNames) {
const entry = getFunction(fnName)
if (!entry) continue
const denial = checkAuth(entry.auth, identity)
if (denial) return authDenialResponse(denial)
}
// Resolve effective rev (max across functions) and cache policy (min TTL)
let effectiveRev = 0
for (const fnName of fnNames) {
@@ -133,6 +193,7 @@ export async function handleContextFetch(
export async function handleMutationCall(
fnName: string,
args: Record<string, any>,
identity: Identity = ANONYMOUS,
): Promise<MizanResponse> {
const entry = getFunction(fnName)
@@ -153,6 +214,10 @@ export async function handleMutationCall(
}
}
// Auth enforcement — after private rejection, before execution.
const denial = checkAuth(entry.auth, identity)
if (denial) return authDenialResponse(denial)
try {
const argValues = entry.params.map(p => args[p.name])
const result = await entry.fn(...argValues)