feat(devops): admin-only info, public version, dep checks, role hardening
Backend: - /api/devops/info now requires admin role (was: any authenticated user). Exposes env keys, dep checks, and socket counts — admin-only by design. - New /api/devops/version (public, no auth) returns build SHA/branch/image for ops/CI rollback verification. - Dep checks: live ping for Cosmos (trading_users) and platform-service. - Service version read dynamically via readServiceVersion(import.meta.url) — no more hardcoded '0.1.0'. - extra: socketIoConnections + tradingApiUrl for runtime debugging. - saveCurrentUserProfile no longer accepts client-supplied role — prevents drift with platform JWT (which is authoritative). Web: - DevOps tab is now admin-only (gated behind isAdmin like Bot Config and Admin Panel). Both the section list and content render are guarded. - Service version baked into bundle via Vite `define` (__WEB_SERVICE_VERSION__) read from web/package.json — no more hardcoded VERSION constant. - Bumps @bytelyst/devops dep to ^0.1.2. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
74400fda70
commit
4763a9a9d1
@ -77,7 +77,7 @@
|
||||
"socket.io": "^4.8.3",
|
||||
"winston": "^3.19.0",
|
||||
"@bytelyst/telemetry-client": "*",
|
||||
"@bytelyst/devops": "^0.1.1"
|
||||
"@bytelyst/devops": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.3",
|
||||
|
||||
@ -3,7 +3,8 @@ import { createServer } from 'http';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import cors from 'cors';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { collectDevopsInfo } from '@bytelyst/devops/server';
|
||||
import { collectDevopsInfo, dependencyCheck, getBuildInfo, httpDependencyCheck, readServiceVersion } from '@bytelyst/devops/server';
|
||||
import { getContainer } from '@bytelyst/cosmos';
|
||||
import logger from '../utils/logger.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@ -2365,13 +2366,31 @@ export class ApiServer {
|
||||
res.json(flags);
|
||||
});
|
||||
|
||||
// ── DevOps info: build, runtime, config (auth required) ──────────────
|
||||
this.app.get('/api/devops/info', this.requireAuth, async (_req, res) => {
|
||||
// ── DevOps version: PUBLIC, no auth (build SHA + branch + image) ─────
|
||||
// Useful for ops/CI health checks and rollback verification.
|
||||
this.app.get('/api/devops/version', (_req, res) => {
|
||||
res.json(getBuildInfo());
|
||||
});
|
||||
|
||||
// ── DevOps info: full payload (admin-only — exposes env keys, deps) ──
|
||||
this.app.get('/api/devops/info', this.requireAuth, this.requireAdmin, async (_req, res) => {
|
||||
try {
|
||||
const info = await collectDevopsInfo({
|
||||
productId: config.PRODUCT_ID || 'invttrdg',
|
||||
serviceName: 'trading-backend',
|
||||
serviceVersion: process.env.npm_package_version || '0.1.0',
|
||||
serviceVersion: readServiceVersion(import.meta.url),
|
||||
dependencyChecks: [
|
||||
() => dependencyCheck('cosmos', async () => {
|
||||
// Lightweight ping: read database properties.
|
||||
const c = getContainer('trading_users');
|
||||
await c.items.query('SELECT TOP 1 c.id FROM c').fetchNext();
|
||||
}),
|
||||
() => httpDependencyCheck('platform-service', `${config.PLATFORM_API_URL}/health`),
|
||||
],
|
||||
extra: {
|
||||
socketIoConnections: this.io?.engine?.clientsCount ?? 0,
|
||||
tradingApiUrl: config.PLATFORM_API_URL,
|
||||
},
|
||||
});
|
||||
res.json(info);
|
||||
} catch (error: any) {
|
||||
|
||||
@ -719,7 +719,10 @@ export async function saveCurrentUserProfile(
|
||||
...input,
|
||||
user_id: userId,
|
||||
email: String(input.email ?? existing.email ?? fallback.email ?? ''),
|
||||
role: String(input.role ?? existing.role ?? fallback.role ?? 'member'),
|
||||
// Role is intentionally NOT persisted from client input — JWT role is the
|
||||
// authoritative source. We keep the existing stored role for backward
|
||||
// compatibility but it's overridden on read at the API layer.
|
||||
role: String(existing.role ?? fallback.role ?? 'member'),
|
||||
first_name: String(input.first_name ?? existing.first_name ?? fallback.first_name ?? ''),
|
||||
last_name: String(input.last_name ?? existing.last_name ?? fallback.last_name ?? ''),
|
||||
trade_enable: Boolean(input.trade_enable ?? existing.trade_enable ?? fallback.trade_enable ?? true),
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"react-router-dom": "^7.14.2",
|
||||
"recharts": "^3.6.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"@bytelyst/devops": "^0.1.1"
|
||||
"@bytelyst/devops": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
|
||||
@ -8,7 +8,9 @@ import { DevopsPanel, type DevopsInfo } from '@bytelyst/devops/ui';
|
||||
import { getPlatformAccessToken } from '../lib/authSession';
|
||||
import { tradingRuntime } from '../lib/runtime';
|
||||
|
||||
const VERSION = '0.1.0';
|
||||
// Injected by Vite at build time (see web/vite.config.ts → define).
|
||||
declare const __WEB_SERVICE_VERSION__: string;
|
||||
const VERSION = typeof __WEB_SERVICE_VERSION__ !== 'undefined' ? __WEB_SERVICE_VERSION__ : '0.0.0';
|
||||
|
||||
async function fetchBackendInfo(): Promise<DevopsInfo> {
|
||||
const token = await getPlatformAccessToken();
|
||||
|
||||
@ -17,13 +17,13 @@ export function SettingsView() {
|
||||
'Account',
|
||||
...(isAdmin ? ['Bot Config' as SettingsSection] : []),
|
||||
...(isAdmin ? ['Admin Panel' as SettingsSection] : []),
|
||||
'DevOps',
|
||||
...(isAdmin ? ['DevOps' as SettingsSection] : []),
|
||||
];
|
||||
const requestedSection = searchParams.get('section');
|
||||
const initialSection = useMemo<SettingsSection>(() => {
|
||||
if (requestedSection === 'Bot Config' && isAdmin) return 'Bot Config';
|
||||
if (requestedSection === 'Admin Panel' && isAdmin) return 'Admin Panel';
|
||||
if (requestedSection === 'DevOps') return 'DevOps';
|
||||
if (requestedSection === 'DevOps' && isAdmin) return 'DevOps';
|
||||
return 'Account';
|
||||
}, [requestedSection, isAdmin]);
|
||||
const [section, setSection] = useState<SettingsSection>(initialSection);
|
||||
@ -64,7 +64,7 @@ export function SettingsView() {
|
||||
{section === 'Account' && <SettingsTab botState={botState} />}
|
||||
{section === 'Bot Config' && isAdmin && <ConfigTab />}
|
||||
{section === 'Admin Panel' && isAdmin && <AdminTab botState={botState} socket={socket} />}
|
||||
{section === 'DevOps' && <DevOpsTab />}
|
||||
{section === 'DevOps' && isAdmin && <DevOpsTab />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,6 +9,10 @@ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Read service version from package.json so DevOps panel reflects the real version.
|
||||
const webPackageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf8')) as { version?: string };
|
||||
const webServiceVersion = webPackageJson.version || '0.0.0';
|
||||
|
||||
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
||||
const monacoEditorPath = path.resolve(__dirname, 'node_modules/monaco-editor');
|
||||
const workspaceRoot = path.resolve(__dirname, '..', '..');
|
||||
@ -32,6 +36,9 @@ function bytelystAlias(pkg: string): string {
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
__WEB_SERVICE_VERSION__: JSON.stringify(webServiceVersion),
|
||||
},
|
||||
// Shared files (../shared/*.ts) live outside web/ so Vite resolves their imports
|
||||
// from the repo root where @bytelyst/* are not installed. Redirect all @bytelyst/*
|
||||
// imports first to web/node_modules, then fall back to the monorepo vendor/ dir.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user