- A site the Hub answers INSTALL_NOT_FOUND for drops its install and licence, so it no longer shows "Active" on the old token. - Unlicensed is read-only like suspended; read-only also blocks editing events, options and variants. LICENCE_CHECK_DISABLED (ignored in production) for e2e. - Heartbeat 30 s after the server starts and after a licence key is connected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.5 KiB
JavaScript
88 lines
3.5 KiB
JavaScript
const prisma = require('../config/db');
|
|
const { safeErrorMessage } = require('../utils/errorUtils');
|
|
const { getClientIp } = require('../utils/requestUtils');
|
|
const { logAdminAction } = require('../utils/adminAudit');
|
|
const { licenceStatus } = require('../hub/licence');
|
|
const hub = require('../hub/client');
|
|
const { APP_VERSION } = require('../hub/report');
|
|
|
|
function compareVersions(a, b) {
|
|
const pa = String(a || '').split('.').map(n => parseInt(n, 10) || 0);
|
|
const pb = String(b || '').split('.').map(n => parseInt(n, 10) || 0);
|
|
for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
|
|
return 0;
|
|
}
|
|
|
|
/** What the admin pages show: licence state, Hub connection, version and update. */
|
|
async function statusPayload({ fresh = false } = {}) {
|
|
const s = await licenceStatus({ fresh });
|
|
return {
|
|
state: s.state,
|
|
readOnly: s.readOnly,
|
|
daysLeft: s.daysLeft,
|
|
endsAt: s.endsAt,
|
|
graceEndsAt: s.graceEndsAt,
|
|
licence: s.claims ? {
|
|
customer: s.claims.customer,
|
|
plan: s.claims.plan,
|
|
status: s.claims.status,
|
|
licenceExpiresAt: s.claims.licenceExpiresAt,
|
|
graceDays: s.claims.graceDays,
|
|
issuedAt: s.claims.issuedAt,
|
|
validUntil: s.claims.validUntil,
|
|
limits: s.claims.limits || {},
|
|
} : null,
|
|
connected: s.connected,
|
|
keyConfigured: s.keyConfigured,
|
|
hubUrl: hub.hubUrl(),
|
|
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
lastError: s.lastError,
|
|
version: APP_VERSION,
|
|
latestVersion: s.latestVersion,
|
|
updateAvailable: !!(s.latestVersion && compareVersions(APP_VERSION, s.latestVersion) < 0),
|
|
};
|
|
}
|
|
|
|
// @desc Licence and Hub status (for the admin banner and Site settings → Licence)
|
|
// @route GET /api/hub/status
|
|
// @access Private/Admin
|
|
const getStatus = async (req, res) => {
|
|
try {
|
|
res.json(await statusPayload());
|
|
} catch (e) {
|
|
res.status(500).json({ message: safeErrorMessage(e) });
|
|
}
|
|
};
|
|
|
|
// @desc Connect this install to the Hub with a licence key (also the /setup licence step)
|
|
// @route POST /api/hub/connect body: { licenceKey }
|
|
// @access Private/Admin
|
|
const connect = async (req, res) => {
|
|
try {
|
|
const licenceKey = String(req.body?.licenceKey || '').trim();
|
|
if (!licenceKey) { res.status(400); throw new Error('Enter your licence key.'); }
|
|
const me = await prisma.user.findUnique({ where: { id: req.user.id }, select: { name: true, email: true } });
|
|
const claims = await hub.connect(licenceKey, me);
|
|
hub.heartbeatSoon();
|
|
logAdminAction({ actorId: req.user.id, actorRole: req.user.role, action: 'hub_connected', targetType: 'hub', metadata: { customer: claims.customer, hubUrl: hub.hubUrl() }, ip: getClientIp(req) });
|
|
res.json(await statusPayload({ fresh: true }));
|
|
} catch (e) {
|
|
const status = e.statusCode || (res.statusCode === 200 ? 400 : res.statusCode);
|
|
res.status(status >= 500 && e.code ? 502 : status).json({ message: e.message || safeErrorMessage(e), code: e.code });
|
|
}
|
|
};
|
|
|
|
// @desc Send a heartbeat now (checks the connection and picks up licence changes)
|
|
// @route POST /api/hub/heartbeat
|
|
// @access Private/Admin
|
|
const heartbeatNow = async (req, res) => {
|
|
try {
|
|
await hub.heartbeat();
|
|
res.json(await statusPayload({ fresh: true }));
|
|
} catch (e) {
|
|
res.status(502).json({ message: e.message || safeErrorMessage(e), code: e.code, status: await statusPayload({ fresh: true }).catch(() => null) });
|
|
}
|
|
};
|
|
|
|
module.exports = { getStatus, connect, heartbeatNow, statusPayload, compareVersions };
|