mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-17 14:08:19 +00:00
refactor(backend): collapse entitlement provider abstraction back to LicenseService (#889)
Removes backend/src/entitlements/ (registry, loadProvider, CommunityEntitlementProvider, types, headers, normalize) and the two abstraction-only tests. Relocates headers/normalize/types to services/license-*.ts. Swaps 22 consumer call sites from getEntitlementProvider() to LicenseService.getInstance(). Drops the Dockerfile install step plus PRO_PACKAGE_VERSION build-arg and github_token BuildKit secret in docker-publish.yml. Removes the now stale no-restricted-imports rule in backend/eslint.config.mjs. Net: 37 files changed, ~700 lines removed, no behavior change. Local dev no longer requires GitHub Packages auth to start the backend. Rationale and revisit conditions in docs/internal/adrs/2026-05-02-collapse-entitlement-provider.md.
This commit is contained in:
@@ -3,7 +3,7 @@ import { DatabaseService } from '../services/DatabaseService';
|
||||
import { CloudBackupService } from '../services/CloudBackupService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { effectiveTier, effectiveVariant } from '../middleware/tierGates';
|
||||
import type { LicenseTier, LicenseVariant } from '../entitlements/types';
|
||||
import type { LicenseTier, LicenseVariant } from '../services/license-types';
|
||||
|
||||
export const dashboardRouter = Router();
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { CloudBackupService } from '../services/CloudBackupService';
|
||||
import { NotificationService } from '../services/NotificationService';
|
||||
import { buildLocalConfigurationStatus, type ConfigurationStatus } from './dashboard';
|
||||
import { PROXY_TIER_HEADER, PROXY_VARIANT_HEADER } from '../entitlements/headers';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { PROXY_TIER_HEADER, PROXY_VARIANT_HEADER } from '../services/license-headers';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
|
||||
const updateTracker = FleetUpdateTrackerService.getInstance();
|
||||
const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
@@ -331,7 +331,7 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp
|
||||
const db = DatabaseService.getInstance();
|
||||
const nodes = db.getNodes();
|
||||
const userId = req.user?.userId ?? 0;
|
||||
const ls = getEntitlementProvider();
|
||||
const ls = LicenseService.getInstance();
|
||||
const localTier = ls.getTier();
|
||||
const localVariant = ls.getVariant();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CacheService } from '../services/CacheService';
|
||||
import { ImageUpdateService } from '../services/ImageUpdateService';
|
||||
import { FileSystemService } from '../services/FileSystemService';
|
||||
import { ComposeService } from '../services/ComposeService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { NotificationService } from '../services/NotificationService';
|
||||
import { enforcePolicyPreDeploy } from '../services/PolicyEnforcement';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
@@ -215,7 +215,7 @@ autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Resp
|
||||
const imageUpdateService = ImageUpdateService.getInstance();
|
||||
const compose = ComposeService.getInstance(req.nodeId);
|
||||
const db = DatabaseService.getInstance();
|
||||
const atomic = getEntitlementProvider().getTier() === 'paid';
|
||||
const atomic = LicenseService.getInstance().getTier() === 'paid';
|
||||
const results: string[] = [];
|
||||
|
||||
for (const stackName of stackNames) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import SelfUpdateService from '../services/SelfUpdateService';
|
||||
import { requireAdmin } from '../middleware/tierGates';
|
||||
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
|
||||
@@ -10,7 +10,7 @@ export const licenseRouter = Router();
|
||||
|
||||
licenseRouter.get('/', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const info = getEntitlementProvider().getLicenseInfo();
|
||||
const info = LicenseService.getInstance().getLicenseInfo();
|
||||
res.json(info);
|
||||
} catch (error) {
|
||||
console.error('[License] Error getting license info:', error);
|
||||
@@ -27,9 +27,9 @@ licenseRouter.post('/activate', async (req: Request, res: Response): Promise<voi
|
||||
res.status(400).json({ error: 'A valid license key is required' });
|
||||
return;
|
||||
}
|
||||
const result = await getEntitlementProvider().activate(license_key.trim());
|
||||
const result = await LicenseService.getInstance().activate(license_key.trim());
|
||||
if (result.success) {
|
||||
res.json({ success: true, license: getEntitlementProvider().getLicenseInfo() });
|
||||
res.json({ success: true, license: LicenseService.getInstance().getLicenseInfo() });
|
||||
} else {
|
||||
res.status(400).json({ error: result.error });
|
||||
}
|
||||
@@ -43,9 +43,9 @@ licenseRouter.post('/deactivate', async (req: Request, res: Response): Promise<v
|
||||
if (rejectApiTokenScope(req, res, LICENSE_SCOPE_MESSAGE)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
const result = await getEntitlementProvider().deactivate();
|
||||
const result = await LicenseService.getInstance().deactivate();
|
||||
if (result.success) {
|
||||
res.json({ success: true, license: getEntitlementProvider().getLicenseInfo() });
|
||||
res.json({ success: true, license: LicenseService.getInstance().getLicenseInfo() });
|
||||
} else {
|
||||
res.status(500).json({ error: result.error });
|
||||
}
|
||||
@@ -57,8 +57,8 @@ licenseRouter.post('/deactivate', async (req: Request, res: Response): Promise<v
|
||||
|
||||
licenseRouter.post('/validate', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await getEntitlementProvider().validate();
|
||||
res.json({ ...result, license: getEntitlementProvider().getLicenseInfo() });
|
||||
const result = await LicenseService.getInstance().validate();
|
||||
res.json({ ...result, license: LicenseService.getInstance().getLicenseInfo() });
|
||||
} catch (error) {
|
||||
console.error('[License] Validation error:', error);
|
||||
res.status(500).json({ error: 'License validation failed' });
|
||||
@@ -67,7 +67,7 @@ licenseRouter.post('/validate', async (_req: Request, res: Response): Promise<vo
|
||||
|
||||
licenseRouter.get('/billing-portal', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await getEntitlementProvider().getBillingPortalUrl();
|
||||
const result = await LicenseService.getInstance().getBillingPortalUrl();
|
||||
if ('error' in result) {
|
||||
res.status(404).json({ error: result.error });
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { ROLE_PERMISSIONS, type PermissionAction } from '../middleware/permissions';
|
||||
|
||||
@@ -30,7 +30,7 @@ permissionsRouter.get('/me', authMiddleware, (req: Request, res: Response): void
|
||||
globalRole,
|
||||
globalPermissions,
|
||||
scopedPermissions,
|
||||
isAdmiral: getEntitlementProvider().getVariant() === 'admiral',
|
||||
isAdmiral: LicenseService.getInstance().getVariant() === 'admiral',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error:', error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { CronExpressionParser } from 'cron-parser';
|
||||
import { DatabaseService, type ScheduledTask } from '../services/DatabaseService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { SchedulerService } from '../services/SchedulerService';
|
||||
import { requirePaid, requireAdmin, requireScheduledTaskTier } from '../middleware/tierGates';
|
||||
import { escapeCsvField } from '../utils/csv';
|
||||
@@ -78,7 +78,7 @@ scheduledTasksRouter.get('/', (req: Request, res: Response): void => {
|
||||
try {
|
||||
let tasks = DatabaseService.getInstance().getScheduledTasks();
|
||||
// Skipper users only see 'update' tasks; Admiral sees all.
|
||||
const ls = getEntitlementProvider();
|
||||
const ls = LicenseService.getInstance();
|
||||
if (ls.getVariant() !== 'admiral') {
|
||||
tasks = tasks.filter(t => t.action === 'update');
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import TrivyService, { SbomFormat } from '../services/TrivyService';
|
||||
import TrivyInstaller from '../services/TrivyInstaller';
|
||||
import { DatabaseService, parsePolicyEvaluation, type VulnerabilityScan } from '../services/DatabaseService';
|
||||
import { FleetSyncService } from '../services/FleetSyncService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { validateImageRef } from '../utils/image-ref';
|
||||
import { applySuppressions } from '../utils/suppression-filter';
|
||||
import { generateSarif } from '../services/SarifExporter';
|
||||
@@ -647,7 +647,7 @@ securityRouter.get('/compare', authMiddleware, (req: Request, res: Response): vo
|
||||
scanId1,
|
||||
scanId2,
|
||||
reqNodeId: req.nodeId,
|
||||
tier: req.proxyTier ?? getEntitlementProvider().getTier(),
|
||||
tier: req.proxyTier ?? LicenseService.getInstance().getTier(),
|
||||
aVulns: aVulns.length,
|
||||
bVulns: bVulns.length,
|
||||
added: added.length,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ComposeService } from '../services/ComposeService';
|
||||
import DockerController from '../services/DockerController';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { CacheService } from '../services/CacheService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { UpdatePreviewService } from '../services/UpdatePreviewService';
|
||||
import { GitSourceService, GitSourceError, repoHost as gitRepoHost } from '../services/GitSourceService';
|
||||
import { enforcePolicyPreDeploy } from '../services/PolicyEnforcement';
|
||||
@@ -587,7 +587,7 @@ stacksRouter.post('/:stackName/deploy', async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!(await runPolicyGate(req, res, stackName, req.nodeId))) return;
|
||||
const debug = isDebugEnabled();
|
||||
const atomic = getEntitlementProvider().getTier() === 'paid';
|
||||
const atomic = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (debug) console.debug('[Stacks:debug] Deploy starting', { stackName, atomic, nodeId: req.nodeId });
|
||||
const t0 = Date.now();
|
||||
await ComposeService.getInstance(req.nodeId).deployStack(stackName, getTerminalWs(), atomic);
|
||||
@@ -601,7 +601,7 @@ stacksRouter.post('/:stackName/deploy', async (req: Request, res: Response) => {
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error('[Stacks] Deploy failed: %s', sanitizeForLog(stackName), error);
|
||||
const rolledBack = getEntitlementProvider().getTier() === 'paid';
|
||||
const rolledBack = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (rolledBack) console.warn('[Stacks] Deploy failed, rolled back: %s', sanitizeForLog(stackName));
|
||||
const message = getErrorMessage(error, 'Failed to deploy stack');
|
||||
notifyActionFailure('deploy', stackName, error);
|
||||
@@ -746,7 +746,7 @@ stacksRouter.post('/:stackName/update', async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!(await runPolicyGate(req, res, stackName, req.nodeId))) return;
|
||||
const debug = isDebugEnabled();
|
||||
const atomic = getEntitlementProvider().getTier() === 'paid';
|
||||
const atomic = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (debug) console.debug('[Stacks:debug] Update starting', { stackName, atomic, nodeId: req.nodeId });
|
||||
const t0 = Date.now();
|
||||
await ComposeService.getInstance(req.nodeId).updateStack(stackName, getTerminalWs(), atomic);
|
||||
@@ -761,7 +761,7 @@ stacksRouter.post('/:stackName/update', async (req: Request, res: Response) => {
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error('[Stacks] Update failed: %s', sanitizeForLog(stackName), error);
|
||||
const rolledBack = getEntitlementProvider().getTier() === 'paid';
|
||||
const rolledBack = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (rolledBack) console.warn(`[Stacks] Update failed, rolled back: ${sanitizeForLog(stackName)}`);
|
||||
notifyActionFailure('update', stackName, error);
|
||||
res.status(500).json({ error: getErrorMessage(error, 'Failed to update'), rolledBack });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { templateService } from '../services/TemplateService';
|
||||
import { FileSystemService } from '../services/FileSystemService';
|
||||
import { ComposeService } from '../services/ComposeService';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { ErrorParser } from '../utils/ErrorParser';
|
||||
import { isValidStackName, isPathWithinBase } from '../utils/validation';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
@@ -124,7 +124,7 @@ templatesRouter.post('/deploy', authMiddleware, async (req: Request, res: Respon
|
||||
}
|
||||
return;
|
||||
}
|
||||
const atomic = getEntitlementProvider().getTier() === 'paid';
|
||||
const atomic = LicenseService.getInstance().getTier() === 'paid';
|
||||
await ComposeService.getInstance(req.nodeId).deployStack(stackName, getTerminalWs(), atomic);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Templates] Deploy completed: ${stackName}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { DatabaseService, type UserRole, type ResourceType } from '../services/DatabaseService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { requirePaid, requireAdmin, requireAdmiral } from '../middleware/tierGates';
|
||||
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
|
||||
@@ -84,7 +84,7 @@ usersRouter.post('/', authMiddleware, async (req: Request, res: Response): Promi
|
||||
}
|
||||
|
||||
// Enforce seat limits based on license variant.
|
||||
const seatLimits = getEntitlementProvider().getSeatLimits();
|
||||
const seatLimits = LicenseService.getInstance().getSeatLimits();
|
||||
if (role === 'admin' && seatLimits.maxAdmins !== null && db.getAdminCount() >= seatLimits.maxAdmins) {
|
||||
res.status(403).json({ error: `Your license allows a maximum of ${seatLimits.maxAdmins} admin account${seatLimits.maxAdmins === 1 ? '' : 's'}. Upgrade to Admiral for unlimited accounts.` });
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Router, type Request, type Response } from 'express';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { WebhookService } from '../services/WebhookService';
|
||||
import { GitSourceService } from '../services/GitSourceService';
|
||||
import { getEntitlementProvider } from '../entitlements/registry';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { requirePaid, requireAdmin } from '../middleware/tierGates';
|
||||
import { webhookTriggerLimiter } from '../middleware/rateLimiters';
|
||||
@@ -122,7 +122,7 @@ webhooksRouter.post('/:id/trigger', webhookTriggerLimiter, async (req: Request,
|
||||
}
|
||||
|
||||
// Trigger only works with an active Skipper or Admiral license.
|
||||
if (getEntitlementProvider().getTier() !== 'paid') {
|
||||
if (LicenseService.getInstance().getTier() !== 'paid') {
|
||||
res.status(403).json({ error: 'This feature requires a Skipper or Admiral license.', code: 'PAID_REQUIRED' });
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ webhooksRouter.post('/:id/trigger', webhookTriggerLimiter, async (req: Request,
|
||||
// Execute asynchronously; return 202 immediately.
|
||||
res.status(202).json({ message: 'Webhook accepted', action });
|
||||
|
||||
const atomic = getEntitlementProvider().getTier() === 'paid';
|
||||
const atomic = LicenseService.getInstance().getTier() === 'paid';
|
||||
svc.execute(id, action, triggerSource, atomic).catch(err => {
|
||||
console.error(`[Webhooks] Execution error for webhook ${id}:`, err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user