New token arch

This commit is contained in:
Urtzi Alfaro
2026-01-10 21:45:37 +01:00
parent cc53037552
commit bf1db7cb9e
26 changed files with 1751 additions and 107 deletions

76
frontend/src/utils/jwt.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* JWT Subscription Utilities
*
* SECURITY NOTE: Subscription data extracted from JWT is for UI/UX purposes ONLY.
* - Use for: Showing/hiding menu items, displaying tier badges, feature previews
* - NEVER use for: Access control decisions, billing logic, feature enforcement
*
* All access control is enforced server-side. The backend will return 402 errors
* if a user attempts to access features their subscription doesn't include,
* regardless of what the frontend displays.
*/
export interface JWTSubscription {
readonly tier: 'starter' | 'professional' | 'enterprise';
readonly status: 'active' | 'pending_cancellation' | 'inactive';
readonly valid_until: string | null;
}
export interface JWTPayload {
user_id: string;
email: string;
exp: number;
iat: number;
iss: string;
tenant_id?: string;
tenant_role?: string;
subscription?: JWTSubscription;
tenant_access?: Array<{
id: string;
role: string;
tier: string;
}>;
[key: string]: any;
}
export function decodeJWT(token: string): JWTPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = parts[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch {
return null;
}
}
export function getSubscriptionFromJWT(token: string | null): Readonly<JWTSubscription> | null {
if (!token) return null;
const payload = decodeJWT(token);
if (!payload?.subscription) return null;
// Return frozen object to prevent modification
return Object.freeze({
tier: payload.subscription.tier,
status: payload.subscription.status,
valid_until: payload.subscription.valid_until
});
}
export function getTenantAccessFromJWT(token: string | null): Array<{
id: string;
role: string;
tier: string;
}> | null {
if (!token) return null;
const payload = decodeJWT(token);
return payload?.tenant_access ?? null;
}
export function getPrimaryTenantIdFromJWT(token: string | null): string | null {
if (!token) return null;
const payload = decodeJWT(token);
return payload?.tenant_id ?? null;
}