mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
e5b1c7b22b
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.
111 lines
4.1 KiB
TypeScript
111 lines
4.1 KiB
TypeScript
import { Router, type Request, type Response } from 'express';
|
|
import { LicenseService } from '../services/LicenseService';
|
|
import SelfUpdateService from '../services/SelfUpdateService';
|
|
import { requireAdmin } from '../middleware/tierGates';
|
|
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
|
|
|
|
const LICENSE_SCOPE_MESSAGE = 'API tokens cannot manage licenses.';
|
|
|
|
export const licenseRouter = Router();
|
|
|
|
licenseRouter.get('/', (_req: Request, res: Response): void => {
|
|
try {
|
|
const info = LicenseService.getInstance().getLicenseInfo();
|
|
res.json(info);
|
|
} catch (error) {
|
|
console.error('[License] Error getting license info:', error);
|
|
res.status(500).json({ error: 'Failed to retrieve license information' });
|
|
}
|
|
});
|
|
|
|
licenseRouter.post('/activate', async (req: Request, res: Response): Promise<void> => {
|
|
if (rejectApiTokenScope(req, res, LICENSE_SCOPE_MESSAGE)) return;
|
|
if (!requireAdmin(req, res)) return;
|
|
try {
|
|
const { license_key } = req.body;
|
|
if (!license_key || typeof license_key !== 'string') {
|
|
res.status(400).json({ error: 'A valid license key is required' });
|
|
return;
|
|
}
|
|
const result = await LicenseService.getInstance().activate(license_key.trim());
|
|
if (result.success) {
|
|
res.json({ success: true, license: LicenseService.getInstance().getLicenseInfo() });
|
|
} else {
|
|
res.status(400).json({ error: result.error });
|
|
}
|
|
} catch (error) {
|
|
console.error('[License] Activation error:', error);
|
|
res.status(500).json({ error: 'License activation failed' });
|
|
}
|
|
});
|
|
|
|
licenseRouter.post('/deactivate', async (req: Request, res: Response): Promise<void> => {
|
|
if (rejectApiTokenScope(req, res, LICENSE_SCOPE_MESSAGE)) return;
|
|
if (!requireAdmin(req, res)) return;
|
|
try {
|
|
const result = await LicenseService.getInstance().deactivate();
|
|
if (result.success) {
|
|
res.json({ success: true, license: LicenseService.getInstance().getLicenseInfo() });
|
|
} else {
|
|
res.status(500).json({ error: result.error });
|
|
}
|
|
} catch (error) {
|
|
console.error('[License] Deactivation error:', error);
|
|
res.status(500).json({ error: 'License deactivation failed' });
|
|
}
|
|
});
|
|
|
|
licenseRouter.post('/validate', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
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' });
|
|
}
|
|
});
|
|
|
|
licenseRouter.get('/billing-portal', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const result = await LicenseService.getInstance().getBillingPortalUrl();
|
|
if ('error' in result) {
|
|
res.status(404).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.json({ url: result.url });
|
|
} catch (error) {
|
|
console.error('[License] Billing portal error:', error);
|
|
res.status(500).json({ error: 'Failed to retrieve billing portal URL' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Respond 202, then trigger the "last breath" self-update after flush.
|
|
* Exported because the fleet "update this node" route reuses the same
|
|
* response shape + post-flush trigger for local-node self-updates.
|
|
*/
|
|
export function scheduleLocalUpdate(res: Response, message: string): void {
|
|
res.status(202).json({ message });
|
|
res.on('finish', () => {
|
|
setTimeout(() => {
|
|
// Defense in depth: triggerUpdate records its own errors into
|
|
// lastUpdateError; guard against an unexpected throw becoming an
|
|
// unhandled rejection.
|
|
SelfUpdateService.getInstance().triggerUpdate().catch((err) => {
|
|
console.error('[SelfUpdate] Unexpected error during triggerUpdate:', err);
|
|
});
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
export const systemUpdateRouter = Router();
|
|
|
|
systemUpdateRouter.post('/update', (req: Request, res: Response): void => {
|
|
if (!requireAdmin(req, res)) return;
|
|
if (!SelfUpdateService.getInstance().isAvailable()) {
|
|
res.status(503).json({ error: 'Self-update unavailable. Sencho must be deployed via Docker Compose.' });
|
|
return;
|
|
}
|
|
scheduleLocalUpdate(res, 'Update initiated. The server will restart shortly.');
|
|
});
|