Files
classroomio/apps/api/src/services/license.ts
T

112 lines
3.4 KiB
TypeScript
Raw Normal View History

2026-03-09 11:18:22 +01:00
import { env } from '@api/config/env';
import { LICENSE_FEATURE, type LicenseFeatureId } from '@cio/utils/license';
export type LicenseStatus = {
valid: boolean;
features: string[];
expiresAt?: string;
};
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
let cachedStatus: LicenseStatus | null = null;
let cacheExpiry = 0;
/** All available license features for non-self-hosted (cloud/SaaS) instances */
const ALL_FEATURES = Object.values(LICENSE_FEATURE);
/** Check if running in self-hosted mode */
function isSelfHosted(): boolean {
return env.PUBLIC_IS_SELFHOSTED === 'true';
}
/**
* Calls the external license verification API.
* or body: { licenseKey: string }.
* Expected response: { valid: boolean, features?: string[], expiresAt?: string }
*/
async function fetchLicenseFromApi(): Promise<LicenseStatus> {
const url = 'https://enterprise-api.classroomio.dev';
const key = env.LICENSE_KEY?.trim();
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ licenseKey: key })
});
if (!response.ok) {
console.error('License API error:', response.status, await response.text());
return { valid: false, features: [] };
}
const data = (await response.json()) as { valid?: boolean; features?: string[]; expiresAt?: string };
return {
valid: !!data.valid,
features: Array.isArray(data.features) ? data.features : [],
expiresAt: typeof data.expiresAt === 'string' ? data.expiresAt : undefined
};
} catch (error) {
console.error('License verification failed:', error);
return { valid: false, features: [] };
}
}
/**
* Gets the current license status. Uses in-memory cache (1 hour TTL).
* For non-self-hosted (cloud/SaaS), returns all features as valid.
*/
export async function getLicenseStatus(): Promise<LicenseStatus> {
const now = Date.now();
if (cachedStatus !== null && now < cacheExpiry) {
return cachedStatus;
}
// Cloud/SaaS has no license restrictions - all features enabled
if (!isSelfHosted()) {
const status: LicenseStatus = { valid: true, features: ALL_FEATURES };
cachedStatus = status;
cacheExpiry = now + CACHE_TTL_MS;
return status;
}
const status = await fetchLicenseFromApi();
cachedStatus = status;
cacheExpiry = now + CACHE_TTL_MS;
return status;
}
/**
* Checks if a specific feature is licensed.
* @param feature - Feature ID from LICENSE_FEATURE
* Non-self-hosted (cloud/SaaS) instances have all features enabled.
*/
export async function isFeatureLicensed(feature: LicenseFeatureId | string): Promise<boolean> {
// Cloud/SaaS has no license restrictions
if (!isSelfHosted()) {
return true;
}
const status = await getLicenseStatus();
if (!status.valid) return false;
return status.features.includes(feature);
}
/**
* Checks if the given feature is licensed. Synchronous version using cached status.
* Returns false if cache is cold (first call will populate async).
* Use getLicenseStatus() or isFeatureLicensed() for accurate checks.
* Non-self-hosted (cloud/SaaS) instances have all features enabled.
*/
export function isFeatureLicensedSync(feature: LicenseFeatureId | string): boolean {
// Cloud/SaaS has no license restrictions
if (!isSelfHosted()) {
return true;
}
if (cachedStatus === null || !cachedStatus.valid) return false;
return cachedStatus.features.includes(feature);
}