FastAPI and TypeScript improved
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user