feat(host-console): gate Host Console behind Admiral tier (#277)

* feat(host-console): gate Host Console behind Admiral tier

Move the Host Console from the Community (free) tier to Admiral,
enforcing the gate at every layer: UI nav visibility, AdmiralGate
wrapper, POST /api/system/console-token endpoint, and the WebSocket
upgrade handler for /api/system/host-console.

* test(auth): mock Admiral license for console-token test

The console-token endpoint now requires Admiral tier. Mock
LicenseService in the test to return pro/team so the happy-path
test passes in CI where no license is activated.
This commit is contained in:
Anso
2026-03-30 15:56:58 -04:00
committed by GitHub
parent ccf6062473
commit b5d3f497cb
7 changed files with 49 additions and 6 deletions
+12 -1
View File
@@ -1,7 +1,7 @@
/**
* Tests for authentication: login, rate limiting, and auth middleware.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import request from 'supertest';
import jwt from 'jsonwebtoken';
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD, TEST_JWT_SECRET } from './helpers/setupTestDb';
@@ -96,6 +96,17 @@ describe('authMiddleware', () => {
// ─── Protected endpoint: console-token ───────────────────────────────────────
describe('POST /api/system/console-token', () => {
// Console-token requires Admiral tier — mock LicenseService for the happy-path test
beforeAll(async () => {
const { LicenseService } = await import('../services/LicenseService');
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('pro');
vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('team');
});
afterAll(() => {
vi.restoreAllMocks();
});
it('returns 401 without authentication (was a security bug - C1 fix)', async () => {
const res = await request(app).post('/api/system/console-token');
expect(res.status).toBe(401);
+8
View File
@@ -2316,6 +2316,13 @@ server.on('upgrade', async (req, socket, head) => {
socket.destroy();
return;
}
// Admiral license gate — host console requires Pro (team variant)
const ls = LicenseService.getInstance();
if (ls.getTier() !== 'pro' || ls.getVariant() !== 'team') {
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
socket.destroy();
return;
}
const hostConsoleWss = new WebSocketServer({ noServer: true });
hostConsoleWss.handleUpgrade(req, socket, head, (ws) => {
hostConsoleWss.close();
@@ -3362,6 +3369,7 @@ app.post('/api/system/console-token', authMiddleware, (req: Request, res: Respon
return;
}
if (!requireAdmin(req, res)) return;
if (!requireAdmiral(req, res)) return;
try {
const settings = DatabaseService.getInstance().getGlobalSettings();
const jwtSecret = settings.auth_jwt_secret;