Fix dashboard runtime deploy dependencies
All checks were successful
deploy / deploy (push) Successful in 22s
All checks were successful
deploy / deploy (push) Successful in 22s
Proof: Include operator dashboard auth module and runtime package manifest updates required for the deployed operator-dashboard and ops-sentinel processes to start successfully. Assumptions: React and ws belong in production dependencies because operator-dashboard now ships in the runtime image; operator-dashboard-auth is part of the deployed backend surface. Still fake: External alert receiver remains unconfigured; this commit only fixes rollout blockers for the runtime-health/dashboard deploy.
This commit is contained in:
parent
0b7e5e2e6c
commit
b8d731408e
3 changed files with 1989 additions and 6 deletions
1833
package-lock.json
generated
1833
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -12,12 +12,23 @@
|
|||
"ops:sentinel": "node src/apps/ops-sentinel.mjs",
|
||||
"strategy:engine": "node src/apps/strategy-engine.mjs",
|
||||
"trade:executor": "node src/apps/trade-executor.mjs",
|
||||
"operator:dashboard": "node src/apps/operator-dashboard.mjs",
|
||||
"operator-dashboard:build": "vite build --config vite.operator-dashboard.config.mjs",
|
||||
"operator-dashboard:dev": "bash scripts/dev/operator-dashboard-dev.sh",
|
||||
"operator-dashboard:forward": "bash scripts/dev/operator-dashboard-forward.sh",
|
||||
"start": "node index.mjs",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"kafkajs": "^2.2.4",
|
||||
"near-api-js": "^7.2.0",
|
||||
"pg": "^8.20.0"
|
||||
"pg": "^8.20.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
149
src/core/operator-dashboard-auth.mjs
Normal file
149
src/core/operator-dashboard-auth.mjs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
const DEFAULT_SESSION_COOKIE_NAME = 'operator_dashboard_session';
|
||||
|
||||
export function resolveDashboardRequestAuth({
|
||||
mode = 'stub',
|
||||
authorizationHeader = '',
|
||||
cookieHeader = '',
|
||||
username = 'admin',
|
||||
password = '',
|
||||
sessionCookieName = DEFAULT_SESSION_COOKIE_NAME,
|
||||
} = {}) {
|
||||
if (mode === 'stub') {
|
||||
return {
|
||||
authenticated: true,
|
||||
subject: 'local-operator',
|
||||
mode,
|
||||
roles: ['operator'],
|
||||
via: 'stub',
|
||||
setSessionCookie: false,
|
||||
sessionCookieName,
|
||||
sessionToken: buildDashboardSessionToken({ username, password }),
|
||||
};
|
||||
}
|
||||
|
||||
if (mode !== 'basic') {
|
||||
return {
|
||||
authenticated: false,
|
||||
subject: null,
|
||||
mode,
|
||||
roles: [],
|
||||
via: 'unsupported',
|
||||
failure_reason: 'unsupported_auth_mode',
|
||||
setSessionCookie: false,
|
||||
sessionCookieName,
|
||||
sessionToken: buildDashboardSessionToken({ username, password }),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionToken = buildDashboardSessionToken({ username, password });
|
||||
const cookies = parseCookieHeader(cookieHeader);
|
||||
if (cookies[sessionCookieName] && cookies[sessionCookieName] === sessionToken) {
|
||||
return {
|
||||
authenticated: true,
|
||||
subject: username,
|
||||
mode,
|
||||
roles: ['operator'],
|
||||
via: 'session_cookie',
|
||||
setSessionCookie: false,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
const basic = parseBasicAuthorizationHeader(authorizationHeader);
|
||||
if (!basic) {
|
||||
return {
|
||||
authenticated: false,
|
||||
subject: null,
|
||||
mode,
|
||||
roles: [],
|
||||
via: 'missing',
|
||||
failure_reason: 'missing_basic_auth',
|
||||
setSessionCookie: false,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
if (basic.username !== username || basic.password !== password) {
|
||||
return {
|
||||
authenticated: false,
|
||||
subject: null,
|
||||
mode,
|
||||
roles: [],
|
||||
via: 'basic_auth',
|
||||
failure_reason: 'invalid_credentials',
|
||||
setSessionCookie: false,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
subject: username,
|
||||
mode,
|
||||
roles: ['operator'],
|
||||
via: 'basic_auth',
|
||||
setSessionCookie: cookies[sessionCookieName] !== sessionToken,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDashboardSessionToken({ username = '', password = '' } = {}) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(`${username}:${password}`)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function buildDashboardSessionCookie({
|
||||
sessionCookieName = DEFAULT_SESSION_COOKIE_NAME,
|
||||
sessionToken,
|
||||
} = {}) {
|
||||
return `${sessionCookieName}=${sessionToken}; Path=/; HttpOnly; SameSite=Lax`;
|
||||
}
|
||||
|
||||
export function buildDashboardAuthChallengeHeader({ realm = 'unrip operator dashboard' } = {}) {
|
||||
return `Basic realm="${realm}"`;
|
||||
}
|
||||
|
||||
export function parseBasicAuthorizationHeader(value = '') {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
const [scheme, token] = value.split(/\s+/, 2);
|
||||
if (!scheme || !token || scheme.toLowerCase() !== 'basic') return null;
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(token, 'base64').toString('utf8');
|
||||
const separatorIndex = decoded.indexOf(':');
|
||||
if (separatorIndex === -1) return null;
|
||||
return {
|
||||
username: decoded.slice(0, separatorIndex),
|
||||
password: decoded.slice(separatorIndex + 1),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseCookieHeader(value = '') {
|
||||
if (!value || typeof value !== 'string') return {};
|
||||
|
||||
return Object.fromEntries(
|
||||
value
|
||||
.split(';')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
const separatorIndex = part.indexOf('=');
|
||||
if (separatorIndex === -1) return [part, ''];
|
||||
return [
|
||||
part.slice(0, separatorIndex).trim(),
|
||||
decodeURIComponent(part.slice(separatorIndex + 1).trim()),
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue