feat: add local auth billing and usage management
This commit is contained in:
83
lib/server/usage-context.ts
Normal file
83
lib/server/usage-context.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { AuthSession } from "@/lib/auth/session";
|
||||
import { hasSuperAdminAccess } from "@/lib/auth/permissions";
|
||||
import {
|
||||
getOrganizationApiConfig,
|
||||
listOrganizations,
|
||||
type OrganizationInfo
|
||||
} from "@/lib/server/organization-client";
|
||||
import type { UsageContext } from "@/lib/types";
|
||||
|
||||
const ORGANIZATION_CACHE_MS = 5 * 60 * 1000;
|
||||
const organizationCache = new Map<string, {
|
||||
expiresAt: number;
|
||||
organizationId: string;
|
||||
organizationName?: string;
|
||||
}>();
|
||||
|
||||
export async function resolvePlatformUsageContext(session: AuthSession): Promise<UsageContext> {
|
||||
const { user } = session;
|
||||
const context: UsageContext = {
|
||||
source: "platform",
|
||||
accountId: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
role: hasSuperAdminAccess(user) ? "super_admin" : user.role,
|
||||
tenantId: user.tenantId,
|
||||
organizationId: user.organizationId,
|
||||
organizationName: user.organizationName
|
||||
};
|
||||
if (user.organizationId) return context;
|
||||
if (!user.tenantId) return context;
|
||||
|
||||
const cached = organizationCache.get(user.tenantId);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return {
|
||||
...context,
|
||||
organizationId: cached.organizationId,
|
||||
organizationName: cached.organizationName
|
||||
};
|
||||
}
|
||||
|
||||
const config = getOrganizationApiConfig(session.accessToken);
|
||||
if (config.defaultOrganizationId && config.tenantId === user.tenantId) {
|
||||
cacheOrganization(user.tenantId, {
|
||||
organizationId: config.defaultOrganizationId
|
||||
});
|
||||
return { ...context, organizationId: config.defaultOrganizationId };
|
||||
}
|
||||
|
||||
const organization = await findOrganizationForTenant(user.tenantId, session.accessToken);
|
||||
if (!organization) return context;
|
||||
cacheOrganization(user.tenantId, organization);
|
||||
return {
|
||||
...context,
|
||||
organizationId: organization.organizationId,
|
||||
organizationName: organization.organizationName
|
||||
};
|
||||
}
|
||||
|
||||
export function clearUsageOrganizationCacheForTests() {
|
||||
organizationCache.clear();
|
||||
}
|
||||
|
||||
async function findOrganizationForTenant(tenantId: string, accessToken?: string): Promise<OrganizationInfo | null> {
|
||||
const contexts = accessToken ? [{ accessToken }, {}] : [{}];
|
||||
for (const context of contexts) {
|
||||
try {
|
||||
const organizations = await listOrganizations(context);
|
||||
const match = organizations.find((item) => String(item.organizationBindTenantId ?? "") === tenantId);
|
||||
if (match) return match;
|
||||
} catch {
|
||||
// Organization lookup must never block a generation request. Missing matches remain unassigned.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cacheOrganization(tenantId: string, organization: { organizationId: string; organizationName?: string }) {
|
||||
organizationCache.set(tenantId, {
|
||||
expiresAt: Date.now() + ORGANIZATION_CACHE_MS,
|
||||
organizationId: organization.organizationId,
|
||||
organizationName: organization.organizationName
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user