95 lines
3.9 KiB
TypeScript
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}:`);
|
|
}
|