[grade=B urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq] DC-134/135/136: Shipdeck integration - data-driven login pages, deploy events, badge suppression
DC-134: /api/v1/auth/login-page serves a generic gated auto-login page for any service registered in services.json without a curated flow (App Selector installs, DC-131 git installs). Curated pages always win; unknown services still 404; sanitizer keeps digits/hyphens (shipdeck-style ids); display names HTML-escaped. Kills the sso-gate.js edit + restart per new install. DC-135: shipdeck journal.jsonl tail worker (startShipdeckWorker) ingests deploy/rollback lifecycle rows into the Security Center as source_type=shipdeck (notice/success, error/failure via verify[] block). DC-136: deploy-aware badge suppression - suppressDuringDeploy() before the bridge call in /deploy and /rollback, clearDeploySuppression() in finally (every exit path incl. rejected fetches), reference-counted for overlapping deploys, 10-min TTL auto-expiry (HEALTH_DEPLOY_SUPPRESS_MAX_MS). 23 new tests across 4 suites; full suite 2923/2923 green. Codex judge: C (r1) -> C (r2) -> B (r3) -> B zero-blockers (r4, urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq).
This commit is contained in:
@@ -267,14 +267,22 @@ module.exports = function(deps) {
|
||||
});
|
||||
|
||||
// Serve service-specific auto-login page (auth enforced by Caddy forward_auth upstream)
|
||||
router.get('/auth/login-page', (req, res) => {
|
||||
const service = (req.query.service || '').replace(/[^a-z]/g, '');
|
||||
router.get('/auth/login-page', asyncHandler(async (req, res) => {
|
||||
// DC-134: ids may contain digits and hyphens (shipdeck installs like
|
||||
// demo-hi3) — keep them, strip everything else. The value is only ever
|
||||
// compared against the curated page keys and service ids.
|
||||
const service = (req.query.service || '').replace(/[^a-z0-9-]/g, '');
|
||||
const configuredHost = siteConfig?.dashboardHost;
|
||||
const dashboardOrigin = typeof configuredHost === 'string'
|
||||
&& /^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(configuredHost)
|
||||
? `https://${configuredHost}`
|
||||
: 'https://status.sami';
|
||||
const html = buildLoginPage(service, dashboardOrigin);
|
||||
// DC-134: read the live services list so any registered service without a
|
||||
// curated auto-login flow still gets a gated generic login page instead
|
||||
// of a 404. Read failure falls back to curated-only behavior.
|
||||
let services = null;
|
||||
try { services = await servicesStateManager.read(); } catch (_) { services = null; }
|
||||
const html = buildLoginPage(service, dashboardOrigin, services);
|
||||
if (!html) return res.status(404).send('Unknown service');
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
@@ -287,12 +295,12 @@ module.exports = function(deps) {
|
||||
// one response only; every other route keeps the strict app-wide policy.
|
||||
res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data:; object-src 'none'; media-src 'self'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'");
|
||||
res.send(html);
|
||||
});
|
||||
}));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
function buildLoginPage(service, dashboardOrigin = 'https://status.sami') {
|
||||
function buildLoginPage(service, dashboardOrigin = 'https://status.sami', services = null) {
|
||||
// Pre-auth check via <meta http-equiv="refresh"> so it fires even when JS is
|
||||
// disabled or blocked. The cookie is sent automatically because we hit the
|
||||
// same origin (plex.sami); if the API returns 200 the user has a valid
|
||||
@@ -401,7 +409,38 @@ ft('chat').then(function(r){return r.text()}).then(function(t){
|
||||
};
|
||||
|
||||
const cfg = pages[service];
|
||||
if (!cfg) return null;
|
||||
if (!cfg) {
|
||||
// DC-134: data-driven fallback. Any service registered in services.json
|
||||
// (App Selector install, DC-131 git install, UI add) gets a generic gated
|
||||
// auto-login page — session was already verified by the SHELL above, so
|
||||
// the body just enters the app the same way the `sec` page does.
|
||||
// ?direct=1 bypasses the Caddy @needsAutoLogin redirect loop. Curated
|
||||
// pages above always win; unknown services still 404 below.
|
||||
const registered = Array.isArray(services) &&
|
||||
services.some(s => s && (s.id === service || s.subdomain === service));
|
||||
if (registered) {
|
||||
const name = (() => {
|
||||
const s = services.find(x => x && (x.id === service || x.subdomain === service));
|
||||
const raw = (s && typeof s.name === 'string' && s.name) || service;
|
||||
// HTML-safe: the title is interpolated into the page shell.
|
||||
return String(raw).replace(/[&<>"']/g, c => (
|
||||
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
||||
));
|
||||
})();
|
||||
const fallback = {
|
||||
title: `Signing in to ${name}...`,
|
||||
bg: '#0a0a0a',
|
||||
accent: '#60a5fa',
|
||||
body: `d.textContent='Session verified, opening dashboard...';go('/?direct=1');`,
|
||||
};
|
||||
return SHELL(fallback.body)
|
||||
.replace(/__TITLE__/g, fallback.title)
|
||||
.replace('__BG__', fallback.bg)
|
||||
.replace('__ACCENT__', fallback.accent)
|
||||
.replace('__DASHBOARD_ORIGIN__', JSON.stringify(dashboardOrigin));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return SHELL(cfg.body)
|
||||
.replace(/__TITLE__/g, cfg.title)
|
||||
.replace('__BG__', cfg.bg)
|
||||
|
||||
Reference in New Issue
Block a user