Add CCP registration page to CML admin panel
Operators can now register with a Control Panel directly from the admin GUI (Services → Control Panel) without SSH access. Uses the existing updateEnvFile + dockerService pattern from the Pangolin setup. New endpoint: /api/ccp-registration (status, register, unregister) New page: ControlPanelPage with form for CCP URL, invite code, agent URL Also passes CCP env vars through docker-compose to the API container. Bunker Admin
This commit is contained in:
parent
c2f7a23b16
commit
67b21ea960
@ -43,6 +43,7 @@ import JitsiMeetPage from '@/pages/JitsiMeetPage';
|
|||||||
import SettingsPage from '@/pages/SettingsPage';
|
import SettingsPage from '@/pages/SettingsPage';
|
||||||
import NavigationSettingsPage from '@/pages/NavigationSettingsPage';
|
import NavigationSettingsPage from '@/pages/NavigationSettingsPage';
|
||||||
import PangolinPage from '@/pages/PangolinPage';
|
import PangolinPage from '@/pages/PangolinPage';
|
||||||
|
import ControlPanelPage from '@/pages/ControlPanelPage';
|
||||||
import ObservabilityPage from '@/pages/ObservabilityPage';
|
import ObservabilityPage from '@/pages/ObservabilityPage';
|
||||||
import DocsAnalyticsPage from '@/pages/DocsAnalyticsPage';
|
import DocsAnalyticsPage from '@/pages/DocsAnalyticsPage';
|
||||||
import AnalyticsOverviewPage from '@/pages/analytics/AnalyticsOverviewPage';
|
import AnalyticsOverviewPage from '@/pages/analytics/AnalyticsOverviewPage';
|
||||||
@ -849,6 +850,14 @@ export default function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="control-panel"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requiredRoles={SYSTEM_ROLES}>
|
||||||
|
<ControlPanelPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="observability"
|
path="observability"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -352,6 +352,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use
|
|||||||
children: [
|
children: [
|
||||||
{ type: 'group', label: 'Infrastructure', children: [
|
{ type: 'group', label: 'Infrastructure', children: [
|
||||||
{ key: '/app/tunnel', icon: <CloudServerOutlined />, label: 'Tunnel' },
|
{ key: '/app/tunnel', icon: <CloudServerOutlined />, label: 'Tunnel' },
|
||||||
|
{ key: '/app/control-panel', icon: <ApiOutlined />, label: 'Control Panel' },
|
||||||
{ key: '/app/observability', icon: <LineChartOutlined />, label: 'Monitoring' },
|
{ key: '/app/observability', icon: <LineChartOutlined />, label: 'Monitoring' },
|
||||||
{ key: '/app/services/nocodb', icon: <DatabaseOutlined />, label: 'Database' },
|
{ key: '/app/services/nocodb', icon: <DatabaseOutlined />, label: 'Database' },
|
||||||
{ key: '/app/services/vaultwarden', icon: <LockOutlined />, label: 'Vault' },
|
{ key: '/app/services/vaultwarden', icon: <LockOutlined />, label: 'Vault' },
|
||||||
|
|||||||
254
admin/src/pages/ControlPanelPage.tsx
Normal file
254
admin/src/pages/ControlPanelPage.tsx
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Space,
|
||||||
|
Descriptions,
|
||||||
|
Tag,
|
||||||
|
Spin,
|
||||||
|
Popconfirm,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
CloudServerOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
DisconnectOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
|
interface RegistrationStatus {
|
||||||
|
registered: boolean;
|
||||||
|
ccpUrl: string | null;
|
||||||
|
agentUrl: string | null;
|
||||||
|
agentRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ControlPanelPage() {
|
||||||
|
const [status, setStatus] = useState<RegistrationStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [registering, setRegistering] = useState(false);
|
||||||
|
const [unregistering, setUnregistering] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const fetchStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data } = await api.get('/api/ccp-registration/status');
|
||||||
|
setStatus(data);
|
||||||
|
} catch {
|
||||||
|
message.error('Failed to load registration status');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { fetchStatus(); }, [fetchStatus]);
|
||||||
|
|
||||||
|
const handleRegister = async (values: { ccpUrl: string; inviteCode: string; agentUrl: string }) => {
|
||||||
|
try {
|
||||||
|
setRegistering(true);
|
||||||
|
const { data } = await api.post('/api/ccp-registration/register', values);
|
||||||
|
if (data.success) {
|
||||||
|
message.success(data.message);
|
||||||
|
fetchStatus();
|
||||||
|
} else {
|
||||||
|
message.error(data.message || 'Registration failed');
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: { message?: string } } } };
|
||||||
|
message.error(error?.response?.data?.error?.message || 'Registration failed');
|
||||||
|
} finally {
|
||||||
|
setRegistering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnregister = async () => {
|
||||||
|
try {
|
||||||
|
setUnregistering(true);
|
||||||
|
const { data } = await api.post('/api/ccp-registration/unregister');
|
||||||
|
if (data.success) {
|
||||||
|
message.success(data.message);
|
||||||
|
form.resetFields();
|
||||||
|
fetchStatus();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('Failed to unregister');
|
||||||
|
} finally {
|
||||||
|
setUnregistering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, textAlign: 'center' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, maxWidth: 800 }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
<div>
|
||||||
|
<Title level={3} style={{ margin: 0 }}>
|
||||||
|
<CloudServerOutlined style={{ marginRight: 8 }} />
|
||||||
|
Control Panel Registration
|
||||||
|
</Title>
|
||||||
|
<Paragraph type="secondary" style={{ marginTop: 8 }}>
|
||||||
|
Register this instance with a Changemaker Control Panel (CCP) for remote management,
|
||||||
|
health monitoring, backups, and upgrades.
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status?.registered ? (
|
||||||
|
// ─── Registered State ────────────────────────────────
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<Alert
|
||||||
|
type="success"
|
||||||
|
message="Registered with Control Panel"
|
||||||
|
description="This instance is connected to a CCP. The agent handles health checks, lifecycle operations, and configuration sync."
|
||||||
|
showIcon
|
||||||
|
icon={<CheckCircleOutlined />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="Connection Details">
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="Control Panel URL">
|
||||||
|
<a href={status.ccpUrl || '#'} target="_blank" rel="noopener noreferrer">
|
||||||
|
{status.ccpUrl}
|
||||||
|
</a>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Agent URL">
|
||||||
|
{status.agentUrl}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Agent Status">
|
||||||
|
{status.agentRunning ? (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">Running</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag icon={<CloseCircleOutlined />} color="error">Not Running</Tag>
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
{!status.agentRunning && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message="Agent container is not running"
|
||||||
|
description="The CCP agent may still be waiting for approval, or it may have stopped. Check the container logs for details."
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||||
|
<Popconfirm
|
||||||
|
title="Unregister from Control Panel?"
|
||||||
|
description="This will stop the agent and remove the CCP connection. You can re-register later with a new invite code."
|
||||||
|
onConfirm={handleUnregister}
|
||||||
|
okText="Yes, Unregister"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DisconnectOutlined />}
|
||||||
|
loading={unregistering}
|
||||||
|
>
|
||||||
|
Unregister
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
// ─── Unregistered State ──────────────────────────────
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
message="Not registered with a Control Panel"
|
||||||
|
description="Enter the CCP URL and invite code provided by your CCP administrator to register this instance for remote management."
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="Register with Control Panel">
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleRegister}
|
||||||
|
style={{ maxWidth: 500 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Control Panel URL"
|
||||||
|
name="ccpUrl"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'CCP URL is required' },
|
||||||
|
{ type: 'url', message: 'Must be a valid URL' },
|
||||||
|
]}
|
||||||
|
extra="The URL of the Changemaker Control Panel (e.g., https://ccp.example.com)"
|
||||||
|
>
|
||||||
|
<Input placeholder="https://ccp.example.com" prefix={<LinkOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Invite Code"
|
||||||
|
name="inviteCode"
|
||||||
|
rules={[{ required: true, message: 'Invite code is required' }]}
|
||||||
|
extra="Single-use code generated by the CCP admin (e.g., ABCD-1234)"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="XXXX-XXXX"
|
||||||
|
style={{ fontFamily: 'monospace', letterSpacing: 2 }}
|
||||||
|
maxLength={20}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Agent URL"
|
||||||
|
name="agentUrl"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Agent URL is required' },
|
||||||
|
{ type: 'url', message: 'Must be a valid URL' },
|
||||||
|
]}
|
||||||
|
extra="How the CCP can reach this machine (must be externally accessible). Include port 7443."
|
||||||
|
>
|
||||||
|
<Input placeholder="https://this-server:7443" prefix={<LinkOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginTop: 24 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
icon={registering ? <LoadingOutlined /> : <CloudServerOutlined />}
|
||||||
|
loading={registering}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Register with Control Panel
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="How it works" size="small">
|
||||||
|
<ol style={{ paddingLeft: 20, margin: 0 }}>
|
||||||
|
<li>Your CCP admin generates an invite code and shares it with you</li>
|
||||||
|
<li>Enter the CCP URL, invite code, and this server's agent URL above</li>
|
||||||
|
<li>The CCP agent starts and sends a registration request to the CCP</li>
|
||||||
|
<li>The CCP admin approves the registration</li>
|
||||||
|
<li>mTLS certificates are issued automatically for secure communication</li>
|
||||||
|
<li>The CCP can now manage this instance remotely</li>
|
||||||
|
</ol>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -212,6 +212,12 @@ const envSchema = z.object({
|
|||||||
ENABLE_SOCIAL: z.string().default('false'),
|
ENABLE_SOCIAL: z.string().default('false'),
|
||||||
ENABLE_PEOPLE: z.string().default('false'),
|
ENABLE_PEOPLE: z.string().default('false'),
|
||||||
ENABLE_ANALYTICS: z.string().default('false'),
|
ENABLE_ANALYTICS: z.string().default('false'),
|
||||||
|
|
||||||
|
// CCP Agent (remote management)
|
||||||
|
ENABLE_CCP_AGENT: z.string().default('false'),
|
||||||
|
CCP_URL: z.string().default(''),
|
||||||
|
CCP_AGENT_URL: z.string().default(''),
|
||||||
|
COMPOSE_PROFILES: z.string().default(''),
|
||||||
TERMUX_API_URL: z.string().default('http://10.0.0.193:5001'),
|
TERMUX_API_URL: z.string().default('http://10.0.0.193:5001'),
|
||||||
TERMUX_API_KEY: z.string().default(''),
|
TERMUX_API_KEY: z.string().default(''),
|
||||||
SMS_DELAY_BETWEEN_MS: z.coerce.number().default(3000),
|
SMS_DELAY_BETWEEN_MS: z.coerce.number().default(3000),
|
||||||
|
|||||||
182
api/src/modules/ccp-registration/ccp-registration.routes.ts
Normal file
182
api/src/modules/ccp-registration/ccp-registration.routes.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { authenticate } from '../../middleware/auth.middleware';
|
||||||
|
import { requireRole } from '../../middleware/rbac.middleware';
|
||||||
|
import { validate } from '../../middleware/validate';
|
||||||
|
import { updateEnvFile } from '../../services/env-writer.service';
|
||||||
|
import { dockerService } from '../../services/docker.service';
|
||||||
|
import { env } from '../../config/env';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// ─── Schemas ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const registerSchema = z.object({
|
||||||
|
ccpUrl: z.string().url().regex(/^https?:\/\//, 'Must be a valid URL'),
|
||||||
|
inviteCode: z.string().min(4).max(20),
|
||||||
|
agentUrl: z.string().url().regex(/^https?:\/\//, 'Must be a valid URL'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── GET /api/ccp-registration/status ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check current CCP registration status by reading env vars.
|
||||||
|
*/
|
||||||
|
router.get(
|
||||||
|
'/status',
|
||||||
|
authenticate,
|
||||||
|
requireRole('SUPER_ADMIN'),
|
||||||
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const ccpUrl = env.CCP_URL || '';
|
||||||
|
const agentUrl = env.CCP_AGENT_URL || '';
|
||||||
|
const enabled = env.ENABLE_CCP_AGENT === 'true';
|
||||||
|
|
||||||
|
// Check if agent container is running
|
||||||
|
let agentRunning = false;
|
||||||
|
try {
|
||||||
|
const status = await dockerService.getContainerStatus('ccp-agent');
|
||||||
|
agentRunning = status.running;
|
||||||
|
} catch {
|
||||||
|
// Container doesn't exist or isn't running
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
registered: enabled && !!ccpUrl,
|
||||||
|
ccpUrl: ccpUrl || null,
|
||||||
|
agentUrl: agentUrl || null,
|
||||||
|
agentRunning,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── POST /api/ccp-registration/register ─────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register this instance with a CCP.
|
||||||
|
* Updates .env and starts the ccp-agent container.
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/register',
|
||||||
|
authenticate,
|
||||||
|
requireRole('SUPER_ADMIN'),
|
||||||
|
validate(registerSchema),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { ccpUrl, inviteCode, agentUrl } = req.body;
|
||||||
|
|
||||||
|
logger.info(`[ccp-registration] Registering with CCP at ${ccpUrl}`);
|
||||||
|
|
||||||
|
// Step 1: Update .env with CCP configuration
|
||||||
|
const envResult = updateEnvFile({
|
||||||
|
ENABLE_CCP_AGENT: 'true',
|
||||||
|
CCP_URL: ccpUrl,
|
||||||
|
CCP_INVITE_CODE: inviteCode,
|
||||||
|
CCP_AGENT_URL: agentUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envResult.success) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: { message: `Failed to update .env: ${envResult.error}`, code: 'ENV_WRITE_FAILED' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Ensure ccp-agent is in COMPOSE_PROFILES
|
||||||
|
const profileResult = updateEnvFile({
|
||||||
|
COMPOSE_PROFILES: addProfile(env.COMPOSE_PROFILES || '', 'ccp-agent'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!profileResult.success) {
|
||||||
|
logger.warn(`[ccp-registration] Failed to update COMPOSE_PROFILES: ${profileResult.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Start the ccp-agent container
|
||||||
|
let agentStarted = false;
|
||||||
|
let agentOutput = '';
|
||||||
|
try {
|
||||||
|
const result = await dockerService.restartContainer('ccp-agent');
|
||||||
|
agentStarted = result.success;
|
||||||
|
agentOutput = result.output;
|
||||||
|
} catch (err) {
|
||||||
|
agentOutput = (err as Error).message;
|
||||||
|
logger.warn(`[ccp-registration] Failed to start agent: ${agentOutput}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[ccp-registration] Registration initiated: env updated, agent ${agentStarted ? 'started' : 'failed to start'}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
envUpdated: true,
|
||||||
|
agentStarted,
|
||||||
|
agentOutput: agentStarted ? undefined : agentOutput,
|
||||||
|
message: agentStarted
|
||||||
|
? 'Registration initiated — agent is phoning home to the CCP. Waiting for admin approval.'
|
||||||
|
: 'Environment configured but agent container failed to start. Check docker compose logs.',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── POST /api/ccp-registration/unregister ───────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove CCP registration and stop the agent.
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/unregister',
|
||||||
|
authenticate,
|
||||||
|
requireRole('SUPER_ADMIN'),
|
||||||
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
logger.info('[ccp-registration] Unregistering from CCP');
|
||||||
|
|
||||||
|
// Clear env vars
|
||||||
|
updateEnvFile({
|
||||||
|
ENABLE_CCP_AGENT: 'false',
|
||||||
|
CCP_URL: '',
|
||||||
|
CCP_INVITE_CODE: '',
|
||||||
|
CCP_AGENT_URL: '',
|
||||||
|
COMPOSE_PROFILES: removeProfile(env.COMPOSE_PROFILES || '', 'ccp-agent'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop the agent container
|
||||||
|
try {
|
||||||
|
// Use docker compose stop instead of up -d
|
||||||
|
const { exec } = await import('child_process');
|
||||||
|
const { promisify } = await import('util');
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
await execAsync('docker compose --profile ccp-agent stop ccp-agent', {
|
||||||
|
cwd: '/app',
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Agent might not be running — that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'CCP registration removed and agent stopped.' });
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function addProfile(current: string, profile: string): string {
|
||||||
|
const profiles = current.split(',').map(p => p.trim()).filter(Boolean);
|
||||||
|
if (!profiles.includes(profile)) profiles.push(profile);
|
||||||
|
return profiles.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProfile(current: string, profile: string): string {
|
||||||
|
return current.split(',').map(p => p.trim()).filter(p => p && p !== profile).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -53,6 +53,7 @@ import { trackingVolunteerRouter, trackingAdminRouter } from './modules/map/trac
|
|||||||
import { geocodingRouter } from './modules/map/geocoding/geocoding.routes';
|
import { geocodingRouter } from './modules/map/geocoding/geocoding.routes';
|
||||||
import { eventsPublicRouter } from './modules/map/events/events.routes';
|
import { eventsPublicRouter } from './modules/map/events/events.routes';
|
||||||
import { pangolinRouter } from './modules/pangolin/pangolin.routes';
|
import { pangolinRouter } from './modules/pangolin/pangolin.routes';
|
||||||
|
import ccpRegistrationRouter from './modules/ccp-registration/ccp-registration.routes';
|
||||||
import { rocketchatRouter } from './modules/rocketchat/rocketchat.routes';
|
import { rocketchatRouter } from './modules/rocketchat/rocketchat.routes';
|
||||||
import { jitsiRouter } from './modules/jitsi/jitsi.routes';
|
import { jitsiRouter } from './modules/jitsi/jitsi.routes';
|
||||||
import { rocketchatWebhookService } from './services/rocketchat-webhook.service';
|
import { rocketchatWebhookService } from './services/rocketchat-webhook.service';
|
||||||
@ -337,6 +338,7 @@ app.use('/api/map/tracking', trackingVolunteerRouter); // Volunteer GPS track
|
|||||||
app.use('/api/map/tracking', trackingAdminRouter); // Admin GPS tracking (MAP_ADMIN+)
|
app.use('/api/map/tracking', trackingAdminRouter); // Admin GPS tracking (MAP_ADMIN+)
|
||||||
app.use('/api/settings', siteSettingsRouter); // Site settings (public GET, SUPER_ADMIN PUT)
|
app.use('/api/settings', siteSettingsRouter); // Site settings (public GET, SUPER_ADMIN PUT)
|
||||||
app.use('/api/pangolin', pangolinRouter); // Pangolin tunnel management (SUPER_ADMIN)
|
app.use('/api/pangolin', pangolinRouter); // Pangolin tunnel management (SUPER_ADMIN)
|
||||||
|
app.use('/api/ccp-registration', ccpRegistrationRouter); // CCP remote management registration (SUPER_ADMIN)
|
||||||
app.use('/api/rocketchat', rocketchatRouter); // Rocket.Chat SSO + status (auth required)
|
app.use('/api/rocketchat', rocketchatRouter); // Rocket.Chat SSO + status (auth required)
|
||||||
app.use('/api/jitsi', jitsiRouter); // Jitsi Meet JWT + status (auth required)
|
app.use('/api/jitsi', jitsiRouter); // Jitsi Meet JWT + status (auth required)
|
||||||
app.use('/api/observability', observabilityRouter); // Observability / monitoring (SUPER_ADMIN)
|
app.use('/api/observability', observabilityRouter); // Observability / monitoring (SUPER_ADMIN)
|
||||||
|
|||||||
@ -106,6 +106,11 @@ services:
|
|||||||
- ENABLE_SOCIAL=${ENABLE_SOCIAL:-false}
|
- ENABLE_SOCIAL=${ENABLE_SOCIAL:-false}
|
||||||
- ENABLE_PEOPLE=${ENABLE_PEOPLE:-false}
|
- ENABLE_PEOPLE=${ENABLE_PEOPLE:-false}
|
||||||
- ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-false}
|
- ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-false}
|
||||||
|
# CCP Agent (remote management)
|
||||||
|
- ENABLE_CCP_AGENT=${ENABLE_CCP_AGENT:-false}
|
||||||
|
- CCP_URL=${CCP_URL:-}
|
||||||
|
- CCP_AGENT_URL=${CCP_AGENT_URL:-}
|
||||||
|
- COMPOSE_PROFILES=${COMPOSE_PROFILES:-}
|
||||||
- TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001}
|
- TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001}
|
||||||
- TERMUX_API_KEY=${TERMUX_API_KEY:-}
|
- TERMUX_API_KEY=${TERMUX_API_KEY:-}
|
||||||
- SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000}
|
- SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000}
|
||||||
|
|||||||
@ -107,6 +107,11 @@ services:
|
|||||||
- ENABLE_SOCIAL=${ENABLE_SOCIAL:-false}
|
- ENABLE_SOCIAL=${ENABLE_SOCIAL:-false}
|
||||||
- ENABLE_PEOPLE=${ENABLE_PEOPLE:-false}
|
- ENABLE_PEOPLE=${ENABLE_PEOPLE:-false}
|
||||||
- ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-false}
|
- ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-false}
|
||||||
|
# CCP Agent (remote management)
|
||||||
|
- ENABLE_CCP_AGENT=${ENABLE_CCP_AGENT:-false}
|
||||||
|
- CCP_URL=${CCP_URL:-}
|
||||||
|
- CCP_AGENT_URL=${CCP_AGENT_URL:-}
|
||||||
|
- COMPOSE_PROFILES=${COMPOSE_PROFILES:-}
|
||||||
- TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001}
|
- TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001}
|
||||||
- TERMUX_API_KEY=${TERMUX_API_KEY:-}
|
- TERMUX_API_KEY=${TERMUX_API_KEY:-}
|
||||||
- SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000}
|
- SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user