web-production-saas-starter/next_b2b_starter/lib/auth/server-permissions.ts
2025-12-16 18:54:41 +04:00

150 lines
4.9 KiB
TypeScript

/**
* Server-Side Permission Utilities
* Fetch permissions from backend API with proper cache control
*/
import type { B2BSessionsAuthenticateResponse } from "stytch";
import { profileRepository } from "@/lib/api/api/repositories/profile-repository";
import type { ProfileResponseDto } from "@/lib/api/api/dto/profile.dto";
import { PERMISSIONS } from "./permissions";
import type { Permission } from "./permissions";
export interface ServerPermissions {
profile: ProfileResponseDto | null;
roles: string[];
permissions: string[];
canViewInvoices: boolean;
canCreateInvoices: boolean;
canUploadInvoices: boolean;
canDeleteInvoices: boolean;
canViewApprovals: boolean;
canApproveInvoices: boolean;
canViewDuplicates: boolean;
canResolveDuplicates: boolean;
canSchedulePayments: boolean;
canExportPayments: boolean;
canExecutePayments: boolean;
canViewAudit: boolean;
canViewOrganization: boolean;
canManageOrganization: boolean;
canManageSubscriptions: boolean; // Derived from org:manage
backendAvailable: boolean;
backendError?: string | null;
}
const KNOWN_PERMISSIONS = new Set<string>(Object.values(PERMISSIONS));
/**
* Compute all permissions for the user based on their session
* Fetches permissions from backend API - NO CACHING to ensure fresh data
*
* Architecture:
* 1. Get email/name from session.member (Stytch session object)
* 2. Fetch permissions from backend /auth/profile/me API
* 3. Backend computes permissions from Stytch RBAC
* 4. Backend validates permissions on every API call (security maintained)
*/
export async function getServerPermissions(
session: B2BSessionsAuthenticateResponse | null
): Promise<ServerPermissions> {
const emptyPermissions: ServerPermissions = {
profile: null,
roles: [],
permissions: [],
canViewInvoices: false,
canCreateInvoices: false,
canUploadInvoices: false,
canDeleteInvoices: false,
canViewApprovals: false,
canApproveInvoices: false,
canViewDuplicates: false,
canResolveDuplicates: false,
canSchedulePayments: false,
canExportPayments: false,
canExecutePayments: false,
canViewAudit: false,
canViewOrganization: false,
canManageOrganization: false,
canManageSubscriptions: false,
backendAvailable: true,
backendError: null,
};
if (!session || !session.session_jwt) {
return emptyPermissions;
}
try {
// Fetch profile with backend-computed permissions
const profile = await profileRepository.getProfile(session.session_jwt);
if (!profile) {
return emptyPermissions;
}
// Defensive: Handle missing or undefined permissions/roles fields
const rawPermissions = Array.isArray(profile.permissions)
? profile.permissions
: [];
const rawRoles = Array.isArray(profile.roles)
? profile.roles
: [];
// Filter to only valid string permissions (trust backend as source of truth)
const permissions = rawPermissions.filter(
(permission): permission is string =>
typeof permission === 'string' && permission.trim().length > 0
);
const roles = rawRoles.filter(
(role): role is string => typeof role === 'string'
);
return {
profile,
roles,
permissions: permissions as string[],
canViewInvoices: permissions.includes(PERMISSIONS.INVOICE_VIEW),
canCreateInvoices: permissions.includes(PERMISSIONS.INVOICE_CREATE),
canUploadInvoices: permissions.includes(PERMISSIONS.INVOICE_UPLOAD),
canDeleteInvoices: permissions.includes(PERMISSIONS.INVOICE_DELETE),
canViewApprovals: permissions.includes(PERMISSIONS.APPROVALS_VIEW),
canApproveInvoices: permissions.includes(PERMISSIONS.APPROVALS_APPROVE),
canViewDuplicates: permissions.includes(PERMISSIONS.DUPLICATES_VIEW),
canResolveDuplicates: permissions.includes(
PERMISSIONS.DUPLICATES_RESOLVE
),
canSchedulePayments: permissions.includes(
PERMISSIONS.PAYMENT_OPTIMIZATION_SCHEDULE
),
canExportPayments: permissions.includes(
PERMISSIONS.PAYMENT_OPTIMIZATION_EXPORT
),
canExecutePayments: permissions.includes(
PERMISSIONS.PAYMENT_OPTIMIZATION_EXECUTE
),
canViewAudit: permissions.includes(PERMISSIONS.AUDIT_VIEW),
canViewOrganization: permissions.includes(PERMISSIONS.ORG_VIEW),
canManageOrganization: permissions.includes(PERMISSIONS.ORG_MANAGE),
canManageSubscriptions: permissions.includes(PERMISSIONS.ORG_MANAGE),
backendAvailable: true,
backendError: null,
};
} catch (error) {
const code = (error as any)?.cause?.code;
const baseMessage = error instanceof Error ? error.message : "Unknown error";
const errorMessage = code ? `${code}: ${baseMessage}` : baseMessage;
console.error('[Auth] Failed to fetch profile from backend:', {
message: baseMessage,
code,
error,
});
return {
...emptyPermissions,
backendAvailable: false,
backendError: errorMessage,
};
}
}