Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/connector/tenantConnector.server.ts
T

95 lines
3.9 KiB
TypeScript

import { cachified, clearCacheKeyPrefix } from "~/utils/cache.server";
import { resolveTenantProductAccess } from "~/utils/entitlements/.server/productAccess.server";
import { getActiveTenantSubscriptions } from "~/utils/services/.server/subscriptionService";
import { evaluateFlagForContext } from "~/utils/services/.server/featureFlagEvaluator.server";
/**
* Konnektor (R-P1b) — das kanonische Pflicht-Gate jeder Modul-Phase.
*
* Zwei Boolean-Checks, auf die jedes Folge-Modul aufsetzt (Reachability / Sidebar-Gating /
* Loader-Guard). Beide bauen auf bestehende Resolver auf, statt sie nachzubauen:
*
* - `tenantHasModule()` — "Darf dieser Tenant dieses Modul nutzen?" Brücke Billing↔Feature-Gate:
* prüft Abo/Entitlement (`SubscriptionFeature` über das aktive Tenant-Abo), Produkt-Blueprint,
* Tenant-Override und ggf. Tenant-Rollen-Permission via {@link resolveTenantProductAccess}.
* Die Modul-Entscheidung wird auf Connector-Ebene gecacht (`getActiveTenantSubscriptions` ist
* bereits gecacht; die zusätzlichen Konfigurations-/Override-/Permission-Queries nicht).
*
* - `tenantHasFlag()` — "Ist dieses Feature-Flag für diesen Kontext aktiv?" Delegiert an den
* v2-Evaluator {@link evaluateFlagForContext}, der bereits eigenen Eval-Cache mitbringt.
*
* Cache-Backing ist die App-Cache-Abstraktion (`~/utils/cache.server`, LRU heute, per
* `redisCacheAdapter` Redis-swappbar — Infra-Schalter, kein Logik-Eingriff).
*/
const MODULE_CACHE_TTL_MS = 60_000;
const MODULE_CACHE_PREFIX = "connector:module:";
export interface TenantModuleAccessInput {
/** Tenant, dessen Abo/Entitlement geprüft wird. */
readonly tenantId: string;
/** Aktueller User (für Tenant-Rollen-Permission + Super-Admin-Bypass). */
readonly userId: string;
/** Modul-/Feature-Code (z.B. "crm-basic"), wie in `SubscriptionFeature.name` / `featureKeys`. */
readonly moduleKey: string;
/** Rollen-Permission-Prüfung umgehen (z.B. System-/Hintergrund-Kontext). */
readonly bypassTenantRolePermission?: boolean;
}
function moduleCacheKey(input: TenantModuleAccessInput): string {
const scope = input.bypassTenantRolePermission ? "bypass" : "rbac";
return `${MODULE_CACHE_PREFIX}${input.tenantId}:${input.userId}:${input.moduleKey}:${scope}`;
}
/**
* Konnektor-Gate: Ist das Modul für den Tenant freigeschaltet?
*
* @returns `true`, wenn der Tenant das Modul nutzen darf (Abo + Blueprint + Override + Rolle ok).
*/
export async function tenantHasModule(input: TenantModuleAccessInput): Promise<boolean> {
return cachified({
key: moduleCacheKey(input),
ttl: MODULE_CACHE_TTL_MS,
getFreshValue: async () => {
const subscription = await getActiveTenantSubscriptions(input.tenantId);
const decision = await resolveTenantProductAccess({
tenantId: input.tenantId,
userId: input.userId,
featureKey: input.moduleKey,
subscription,
bypassTenantRolePermission: input.bypassTenantRolePermission,
});
return decision.allowed;
},
});
}
export interface TenantFlagContext {
/** Flag-Name/-Key wie in der FeatureFlag-Tabelle. */
readonly flagKey: string;
readonly tenantId?: string | null;
readonly userId?: string | null;
}
/**
* Konnektor-Gate: Ist das Feature-Flag für den Kontext aktiv?
*
* @returns `true`, wenn das Flag (global/Tenant/User-scoped + Regeln) enabled ist.
*/
export async function tenantHasFlag(input: TenantFlagContext): Promise<boolean> {
const result = await evaluateFlagForContext(input.flagKey, {
flagName: input.flagKey,
tenantId: input.tenantId ?? null,
userId: input.userId ?? null,
});
return result.enabled;
}
/**
* Verwirft alle gecachten Modul-Entscheidungen eines Tenants. Aufrufen nach Abo-/Override-/
* Rollen-Änderungen (z.B. Stripe-Webhook, Admin-Plan-Wechsel), damit das Gate sofort nachzieht.
*/
export function invalidateTenantModuleAccessCache(tenantId: string): void {
clearCacheKeyPrefix(`${MODULE_CACHE_PREFIX}${tenantId}:`);
}