"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.pangolinRouter = void 0; const express_1 = require("express"); const zod_1 = require("zod"); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const rate_limit_redis_1 = __importDefault(require("rate-limit-redis")); const fs_1 = require("fs"); const path_1 = require("path"); const yaml_1 = require("yaml"); const auth_middleware_1 = require("../../middleware/auth.middleware"); const rbac_middleware_1 = require("../../middleware/rbac.middleware"); const redis_1 = require("../../config/redis"); const pangolin_client_1 = require("../../services/pangolin.client"); const env_writer_service_1 = require("../../services/env-writer.service"); const docker_service_1 = require("../../services/docker.service"); const env_1 = require("../../config/env"); const logger_1 = require("../../utils/logger"); // Rate limiter for expensive Pangolin operations const pangolinSetupLimiter = (0, express_rate_limit_1.default)({ windowMs: 5 * 60 * 1000, // 5 minutes max: 3, // Max 3 operations per 5 minutes per user keyGenerator: (req) => `pangolin-setup:${req.user?.id || req.ip}`, standardHeaders: true, legacyHeaders: false, store: new rate_limit_redis_1.default({ sendCommand: (command, ...args) => redis_1.redis.call(command, ...args), prefix: 'rl:pangolin-setup:', }), message: { error: { message: 'Too many Pangolin setup operations. Please wait 5 minutes before trying again.', code: 'PANGOLIN_RATE_LIMIT_EXCEEDED', }, }, }); /** * Load resource definitions from YAML config file */ function loadResourceDefinitions() { try { const configPath = (0, path_1.join)(__dirname, '../../../configs/pangolin/resources.yml'); const fileContent = (0, fs_1.readFileSync)(configPath, 'utf8'); const config = (0, yaml_1.parse)(fileContent); if (!config || !Array.isArray(config.resources)) { logger_1.logger.error('Invalid resources.yml format'); return []; } // Apply defaults for target_ip/target_port const resources = config.resources.map(r => ({ ...r, target_ip: r.target_ip || 'nginx', target_port: r.target_port || 80, })); logger_1.logger.info(`Loaded ${resources.length} resource definitions from YAML`); return resources; } catch (err) { logger_1.logger.error('Failed to load resource definitions:', err); return []; } } /** * Validate container is running before creating resource */ async function validateContainer(def) { try { if (def.profile === 'monitoring') { const grafanaStatus = await docker_service_1.dockerService.getContainerStatus('grafana-changemaker'); if (!grafanaStatus.running) { return { valid: false, warning: `Monitoring profile not active, skipping ${def.name}`, shouldSkip: true, }; } } const status = await docker_service_1.dockerService.getContainerStatus(def.container); if (!status.running) { if (def.required) { return { valid: false, warning: `Required container not running: ${def.container}`, shouldSkip: false, }; } else { return { valid: false, warning: `Optional container not running: ${def.container}, skipping`, shouldSkip: true, }; } } return { valid: true, shouldSkip: false }; } catch (err) { logger_1.logger.error(`Container validation failed for ${def.container}:`, err); return { valid: false, warning: `Failed to check container status: ${def.container}`, shouldSkip: !def.required, }; } } /** * Suggest next available subnet based on existing sites */ function suggestNextSubnet(sites) { if (!sites || sites.length === 0) { return '100.89.128.4/30'; } const subnets = sites .map(s => s.subnet) .filter((s) => Boolean(s)) .map(s => { const match = s.match(/^100\.89\.128\.(\d+)\/30$/); return { subnet: s, octet: match ? parseInt(match[1], 10) : 0 }; }) .sort((a, b) => a.octet - b.octet); if (subnets.length === 0) { return '100.89.128.4/30'; } const lastSubnet = subnets[subnets.length - 1].subnet; if (!lastSubnet) { return '100.89.128.4/30'; } const match = lastSubnet.match(/^100\.89\.128\.(\d+)\/30$/); if (match && match[1]) { const lastOctet = parseInt(match[1], 10); const nextOctet = lastOctet + 4; if (nextOctet <= 252) { return `100.89.128.${nextOctet}/30`; } } return '100.89.128.4/30'; } const router = (0, express_1.Router)(); // --- All endpoints require SUPER_ADMIN authentication --- router.use(auth_middleware_1.authenticate); router.use((0, rbac_middleware_1.requireRole)('SUPER_ADMIN')); // GET /api/pangolin/status — Health + connection info router.get('/status', async (_req, res) => { try { const configured = pangolin_client_1.pangolinClient.configured; let healthy = false; if (configured) { healthy = await pangolin_client_1.pangolinClient.healthCheck(); } res.json({ configured, healthy, pangolinUrl: env_1.env.PANGOLIN_API_URL || null, orgId: env_1.env.PANGOLIN_ORG_ID || null, siteId: env_1.env.PANGOLIN_SITE_ID || null, newtConfigured: !!(env_1.env.PANGOLIN_NEWT_ID && env_1.env.PANGOLIN_NEWT_SECRET), }); } catch (err) { logger_1.logger.error('Pangolin status check failed:', err); res.status(500).json({ error: { message: 'Failed to check Pangolin status', code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/config — Current env config router.get('/config', (_req, res) => { res.json({ pangolinApiUrl: env_1.env.PANGOLIN_API_URL || null, pangolinEndpoint: env_1.env.PANGOLIN_ENDPOINT || null, orgId: env_1.env.PANGOLIN_ORG_ID || null, siteId: env_1.env.PANGOLIN_SITE_ID || null, newtId: env_1.env.PANGOLIN_NEWT_ID || null, newtConfigured: !!(env_1.env.PANGOLIN_NEWT_ID && env_1.env.PANGOLIN_NEWT_SECRET), domain: env_1.env.DOMAIN, }); }); // GET /api/pangolin/newt-status — Check newt container status router.get('/newt-status', async (_req, res) => { try { const containerStatus = await docker_service_1.dockerService.getContainerStatus('newt-changemaker'); res.json({ newtConfigured: !!(env_1.env.PANGOLIN_NEWT_ID && env_1.env.PANGOLIN_NEWT_SECRET), containerRunning: containerStatus.running, containerStatus: containerStatus.status, ready: containerStatus.running && !!(env_1.env.PANGOLIN_NEWT_ID && env_1.env.PANGOLIN_NEWT_SECRET), error: containerStatus.error, }); } catch (err) { logger_1.logger.error('Newt status check failed:', err); res.status(500).json({ error: { message: 'Failed to check newt status', code: 'DOCKER_ERROR' } }); } }); // POST /api/pangolin/newt-restart — Restart/recreate newt container router.post('/newt-restart', async (_req, res) => { try { if (!env_1.env.PANGOLIN_NEWT_ID || !env_1.env.PANGOLIN_NEWT_SECRET) { res.status(400).json({ error: { message: 'Newt credentials not configured. Run setup first and update .env file.', code: 'NOT_CONFIGURED' }, }); return; } const result = await docker_service_1.dockerService.restartContainer('newt'); if (!result.success) { res.status(500).json({ error: { message: 'Failed to restart container', code: 'DOCKER_ERROR' }, details: result.output, }); return; } res.json({ success: true, message: 'Newt container restarted successfully', output: result.output, }); } catch (err) { logger_1.logger.error('Newt restart failed:', err); res.status(500).json({ error: { message: 'Failed to restart newt container', code: 'DOCKER_ERROR' } }); } }); // GET /api/pangolin/sites — List sites router.get('/sites', async (_req, res) => { try { const sites = await pangolin_client_1.pangolinClient.listSites(); res.json({ sites }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/exit-nodes — List available exit nodes router.get('/exit-nodes', async (_req, res) => { try { const exitNodes = await pangolin_client_1.pangolinClient.listExitNodes(); res.json({ exitNodes }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.warn('Failed to list exit nodes:', msg); res.status(500).json({ error: { message: 'Failed to retrieve exit nodes from Pangolin API', code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/resource-definitions — List resource definitions from YAML config router.get('/resource-definitions', (_req, res) => { try { const resourceDefs = loadResourceDefinitions(); res.json({ domain: env_1.env.DOMAIN, resources: resourceDefs.map(def => ({ name: def.name, subdomain: def.subdomain, fullDomain: def.subdomain ? `${def.subdomain}.${env_1.env.DOMAIN}` : env_1.env.DOMAIN, port: def.port, container: def.container, target_ip: def.target_ip, target_port: def.target_port, required: def.required, })), }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('Failed to load resource definitions:', err); res.status(500).json({ error: { message: msg, code: 'CONFIG_ERROR' } }); } }); // GET /api/pangolin/resource-status — Cross-referenced live status of all resources router.get('/resource-status', async (_req, res) => { try { if (!pangolin_client_1.pangolinClient.configured) { res.status(400).json({ error: { message: 'Pangolin not configured', code: 'NOT_CONFIGURED' } }); return; } const domain = env_1.env.DOMAIN; const siteId = env_1.env.PANGOLIN_SITE_ID || ''; const resourceDefs = loadResourceDefinitions(); const liveResources = await pangolin_client_1.pangolinClient.listResources(); // Build lookup by fullDomain const liveByDomain = new Map(liveResources.map(r => [r.fullDomain || '', r])); // Cross-reference YAML definitions with live state const resources = resourceDefs.map(def => { const fullDomain = def.subdomain ? `${def.subdomain}.${domain}` : domain; const live = liveByDomain.get(fullDomain); // Find matching target (from inline targets array) const targets = live?.targets || []; const target = targets[0]; // Primary target const expectedIp = def.target_ip; const expectedPort = def.target_port; const targetCorrect = target ? target.ip === expectedIp && target.port === expectedPort : false; // Remove from map so we can find "extras" later if (live) liveByDomain.delete(fullDomain); return { name: def.name, subdomain: def.subdomain, fullDomain, required: def.required, container: def.container, profile: def.profile || null, expectedTargetIp: expectedIp, expectedTargetPort: expectedPort, exists: !!live, resourceId: live?.resourceId || null, hasTarget: !!target, targetCorrect, actualTargetIp: target?.ip || null, actualTargetPort: target?.port || null, targetId: target?.targetId || null, targetEnabled: target?.enabled ?? null, ssl: live?.ssl ?? null, sso: live?.blockAccess ?? null, blockAccess: live?.blockAccess ?? null, enabled: live?.active ?? null, }; }); // Remaining entries in liveByDomain are "extras" — in Pangolin but not in YAML const extras = Array.from(liveByDomain.values()).map(r => { const target = r.targets?.[0]; return { name: r.name, subdomain: r.subdomain || null, fullDomain: r.fullDomain || '', required: false, container: null, profile: null, expectedTargetIp: null, expectedTargetPort: null, exists: true, resourceId: r.resourceId, hasTarget: !!target, targetCorrect: true, // No expected value to compare against actualTargetIp: target?.ip || null, actualTargetPort: target?.port || null, targetId: target?.targetId || null, targetEnabled: target?.enabled ?? null, ssl: r.ssl ?? null, sso: r.blockAccess ?? null, blockAccess: r.blockAccess ?? null, enabled: r.active ?? null, }; }); // Summary counts const healthy = resources.filter(r => r.exists && r.hasTarget && r.targetCorrect).length; const misconfigured = resources.filter(r => r.exists && (!r.hasTarget || !r.targetCorrect)).length; const missing = resources.filter(r => !r.exists).length; res.json({ resources, extras, summary: { total: resources.length, healthy, misconfigured, missing, extras: extras.length, }, siteId, domain, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('Resource status check failed:', err); res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/resources — List resources router.get('/resources', async (_req, res) => { try { const resources = await pangolin_client_1.pangolinClient.listResources(); res.json({ resources }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // POST /api/pangolin/setup — Full automated setup: site + resources + targets + .env update router.post('/setup', pangolinSetupLimiter, async (req, res) => { try { if (!pangolin_client_1.pangolinClient.configured) { res.status(400).json({ error: { message: 'Pangolin not configured. Set PANGOLIN_API_URL, PANGOLIN_API_KEY, and PANGOLIN_ORG_ID in .env', code: 'NOT_CONFIGURED' }, }); return; } const domain = env_1.env.DOMAIN; const siteName = req.body?.siteName || `changemaker-${domain}`; const exitNodeId = req.body?.exitNodeId; const autoWriteEnv = req.body?.autoWriteEnv !== false; // Default true // Validate exit node ID if provided if (exitNodeId !== undefined && exitNodeId !== null && exitNodeId !== '') { if (typeof exitNodeId !== 'string' || exitNodeId.length < 3 || exitNodeId.length > 100) { res.status(400).json({ error: { message: 'Invalid exit node ID format', code: 'INVALID_EXIT_NODE' }, }); return; } try { const availableNodes = await pangolin_client_1.pangolinClient.listExitNodes(); if (availableNodes.length > 0) { const node = availableNodes.find(n => n.exitNodeId === exitNodeId); if (!node) { res.status(400).json({ error: { message: 'Exit node not found in available nodes', code: 'EXIT_NODE_NOT_FOUND' }, }); return; } if (!node.online) { logger_1.logger.warn(`Creating site with offline exit node: ${exitNodeId}`); } } } catch { logger_1.logger.warn('Could not verify exit node (endpoint unavailable)'); } } // === STEP 1: Get Newt credentials from Pangolin === logger_1.logger.info('Setup Step 1: Fetching Newt credentials via pickSiteDefaults...'); let newtId; let newtSecret; let address; let usedPickDefaults = false; try { const defaults = await pangolin_client_1.pangolinClient.pickSiteDefaults(); newtId = defaults.newtId; newtSecret = defaults.newtSecret; address = defaults.address; usedPickDefaults = true; logger_1.logger.info(`pickSiteDefaults succeeded: newtId=${newtId}`); } catch (err) { // Fallback: older Pangolin versions may not have this endpoint logger_1.logger.warn('pickSiteDefaults not available, will parse Newt creds from site creation response'); newtId = ''; newtSecret = ''; address = ''; } // === STEP 2: Create site === logger_1.logger.info(`Setup Step 2: Creating site "${siteName}"...`); const sitePayload = { name: siteName, type: 'newt', ...(exitNodeId && { exitNodeId }), }; // Pass Newt credentials from pickSiteDefaults if available if (usedPickDefaults) { sitePayload.newtId = newtId; sitePayload.secret = newtSecret; if (address) sitePayload.address = address; } const site = await pangolin_client_1.pangolinClient.createSite(sitePayload); const siteId = site.siteId; logger_1.logger.info(`Site created: ${siteId}`); // If we didn't use pickSiteDefaults, try to get creds from site response if (!usedPickDefaults && site.newt) { newtId = site.newt.newtId; newtSecret = site.newt.secret; logger_1.logger.info(`Got Newt credentials from site creation response: newtId=${newtId}`); } // === STEP 3: Get domain ID (once, for all resources) === logger_1.logger.info('Setup Step 3: Looking up domain...'); const domains = await pangolin_client_1.pangolinClient.listDomains(); const matchingDomain = domains.find(d => domain.endsWith(d.baseDomain)); if (!matchingDomain) { res.status(400).json({ error: { message: `Domain "${domain}" not registered in Pangolin. Register it first.`, code: 'DOMAIN_NOT_FOUND' }, }); return; } logger_1.logger.info(`Using domain: ${matchingDomain.baseDomain} (ID: ${matchingDomain.domainId})`); // === STEP 4: Create resources + targets === logger_1.logger.info('Setup Step 4: Creating resources and targets...'); const resourceDefs = loadResourceDefinitions(); if (resourceDefs.length === 0) { logger_1.logger.error('No resource definitions found - check configs/pangolin/resources.yml'); } const created = []; const errors = []; const warnings = []; for (const def of resourceDefs) { const fullDomain = def.subdomain ? `${def.subdomain}.${domain}` : domain; // Validate container const validation = await validateContainer(def); if (!validation.valid) { if (validation.shouldSkip) { warnings.push(`${fullDomain}: ${validation.warning}`); logger_1.logger.warn(validation.warning); continue; } else { errors.push(`${fullDomain}: ${validation.warning}`); logger_1.logger.error(validation.warning); continue; } } try { // Step 4a: Create HTTP resource const resourcePayload = { name: def.name, domainId: matchingDomain.domainId, subdomain: def.subdomain || '', http: true, protocol: 'tcp', }; const resource = await pangolin_client_1.pangolinClient.createResource(resourcePayload); const resourceId = resource.resourceId; logger_1.logger.info(`Created resource: ${fullDomain} (ID: ${resourceId})`); // Make resource publicly accessible try { await pangolin_client_1.pangolinClient.updateResource(resourceId, { blockAccess: false }); logger_1.logger.info(`Set ${fullDomain} as publicly accessible`); } catch (updateErr) { logger_1.logger.warn(`Created ${fullDomain} but failed to set public access:`, updateErr); } // Step 4b: Create target (THE MISSING STEP!) let targetCreated = false; try { const targetPayload = { siteId, ip: def.target_ip, port: def.target_port, method: 'http', enabled: true, }; await pangolin_client_1.pangolinClient.createTarget(resourceId, targetPayload); targetCreated = true; logger_1.logger.info(`Created target for ${fullDomain}: ${def.target_ip}:${def.target_port}`); } catch (targetErr) { const msg = targetErr instanceof Error ? targetErr.message : 'Unknown error'; errors.push(`${fullDomain} (target): ${msg}`); logger_1.logger.error(`Failed to create target for ${fullDomain}: ${msg}`); } created.push({ subdomain: def.subdomain || '(root)', name: def.name, resourceId, targetCreated }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; errors.push(`${fullDomain}: ${msg}`); logger_1.logger.warn(`Failed to create resource for ${fullDomain}: ${msg}`); } } // === STEP 5: Auto-write .env file === let envWriteResult = null; if (autoWriteEnv && newtId && newtSecret) { logger_1.logger.info('Setup Step 5: Writing credentials to .env...'); const pangolinEndpoint = env_1.env.PANGOLIN_ENDPOINT || env_1.env.PANGOLIN_API_URL.replace('/v1', ''); envWriteResult = (0, env_writer_service_1.updateEnvFile)({ PANGOLIN_SITE_ID: siteId, PANGOLIN_NEWT_ID: newtId, PANGOLIN_NEWT_SECRET: newtSecret, PANGOLIN_ENDPOINT: pangolinEndpoint, }); if (envWriteResult.success) { logger_1.logger.info('Credentials written to .env successfully'); } else { logger_1.logger.error(`Failed to write .env: ${envWriteResult.error}`); warnings.push(`.env auto-write failed: ${envWriteResult.error}`); } } // === STEP 6: Restart Newt container === let newtRestarted = false; if (autoWriteEnv && envWriteResult?.success) { logger_1.logger.info('Setup Step 6: Restarting Newt container...'); try { const result = await docker_service_1.dockerService.restartContainer('newt'); newtRestarted = result.success; if (!result.success) { warnings.push(`Newt restart failed: ${result.output}`); } } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; warnings.push(`Newt restart error: ${msg}`); } } res.json({ success: true, site: { siteId, name: siteName, }, newt: { newtId: newtId || null, newtSecret: newtSecret ? '***' : null, // Don't expose in response when auto-written credentialsAutoWritten: envWriteResult?.success || false, containerRestarted: newtRestarted, }, resources: { created: created.length, total: resourceDefs.length, items: created, errors, warnings, }, envUpdate: envWriteResult ? { success: envWriteResult.success, updated: envWriteResult.updated, added: envWriteResult.added, error: envWriteResult.error, } : null, // Fallback instructions if auto-write failed ...(!envWriteResult?.success && newtId && { manualInstructions: [ 'Auto-write to .env failed. Add these manually:', `PANGOLIN_SITE_ID=${siteId}`, `PANGOLIN_NEWT_ID=${newtId}`, `PANGOLIN_NEWT_SECRET=${newtSecret}`, `PANGOLIN_ENDPOINT=${env_1.env.PANGOLIN_ENDPOINT || env_1.env.PANGOLIN_API_URL.replace('/v1', '')}`, 'Then restart the newt container: docker compose up -d newt', ], }), }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('Pangolin setup failed:', err); res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // POST /api/pangolin/sync — Sync resources (create missing, verify targets) router.post('/sync', pangolinSetupLimiter, async (_req, res) => { try { if (!pangolin_client_1.pangolinClient.configured) { res.status(400).json({ error: { message: 'Pangolin not configured', code: 'NOT_CONFIGURED' } }); return; } const siteSlug = env_1.env.PANGOLIN_SITE_ID; if (!siteSlug) { res.status(400).json({ error: { message: 'PANGOLIN_SITE_ID not set. Run setup first.', code: 'NO_SITE' } }); return; } // Resolve numeric siteId from slug (Pangolin targets require numeric siteId) let siteId = siteSlug; if (isNaN(Number(siteSlug))) { const sites = await pangolin_client_1.pangolinClient.listSites(); const match = sites.find(s => s.niceId === siteSlug || s.name === siteSlug); if (match) { siteId = match.siteId; logger_1.logger.info(`Resolved site slug "${siteSlug}" to numeric siteId ${siteId}`); } else { logger_1.logger.warn(`Could not resolve site slug "${siteSlug}" to numeric ID, using as-is`); } } const domain = env_1.env.DOMAIN; const resourceDefs = loadResourceDefinitions(); if (resourceDefs.length === 0) { res.status(500).json({ error: { message: 'No resource definitions found. Check configs/pangolin/resources.yml', code: 'NO_CONFIG' } }); return; } // Get existing resources and domain info const [existing, domains] = await Promise.all([ pangolin_client_1.pangolinClient.listResources(), pangolin_client_1.pangolinClient.listDomains(), ]); const matchingDomain = domains.find(d => domain.endsWith(d.baseDomain)); if (!matchingDomain) { res.status(400).json({ error: { message: `Domain "${domain}" not registered in Pangolin`, code: 'DOMAIN_NOT_FOUND' } }); return; } // Build lookup by full domain const existingByDomain = new Map(existing.map(r => [r.fullDomain || '', r])); const created = []; const targetFixed = []; const skipped = []; const warnings = []; const errors = []; for (const def of resourceDefs) { const fullDomain = def.subdomain ? `${def.subdomain}.${domain}` : domain; // Validate container const validation = await validateContainer(def); if (!validation.valid) { if (validation.shouldSkip) { warnings.push(`${fullDomain}: ${validation.warning}`); continue; } else { errors.push(`${fullDomain}: ${validation.warning}`); continue; } } const existingResource = existingByDomain.get(fullDomain); if (existingResource) { // Resource exists — verify it has a target try { const targets = await pangolin_client_1.pangolinClient.listTargets(existingResource.resourceId); if (targets.length === 0) { // Missing target — create one logger_1.logger.info(`Resource ${fullDomain} has no target, creating one...`); await pangolin_client_1.pangolinClient.createTarget(existingResource.resourceId, { siteId, ip: def.target_ip, port: def.target_port, method: 'http', enabled: true, }); targetFixed.push(fullDomain); logger_1.logger.info(`Created missing target for ${fullDomain}`); } else { skipped.push(fullDomain); } } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; // If listing targets fails, try to create one anyway try { await pangolin_client_1.pangolinClient.createTarget(existingResource.resourceId, { siteId, ip: def.target_ip, port: def.target_port, method: 'http', enabled: true, }); targetFixed.push(fullDomain); } catch (targetErr) { errors.push(`${fullDomain} (target check): ${msg}`); } } } else { // Create new resource + target try { const resource = await pangolin_client_1.pangolinClient.createResource({ name: def.name, domainId: matchingDomain.domainId, subdomain: def.subdomain || '', http: true, protocol: 'tcp', }); // Make publicly accessible (disable SSO auth + blockAccess) try { await pangolin_client_1.pangolinClient.updateResource(resource.resourceId, { sso: false, blockAccess: false }); } catch { logger_1.logger.warn(`Created ${fullDomain} but failed to set public access`); } // Create target try { await pangolin_client_1.pangolinClient.createTarget(resource.resourceId, { siteId, ip: def.target_ip, port: def.target_port, method: 'http', enabled: true, }); } catch (targetErr) { const msg = targetErr instanceof Error ? targetErr.message : 'Unknown error'; errors.push(`${fullDomain} (target): ${msg}`); } created.push(fullDomain); logger_1.logger.info(`Created resource + target: ${fullDomain}`); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; errors.push(`${fullDomain}: ${msg}`); logger_1.logger.error(`Failed to create resource ${fullDomain}:`, err); } } } res.json({ success: true, created: created.length, targetFixed: targetFixed.length, skipped: skipped.length, warnings: warnings.length, errors: errors.length, details: { created, targetFixed, skipped, warnings, errors }, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('Pangolin sync failed:', err); res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // DELETE /api/pangolin/resource/:id — Delete a resource router.delete('/resource/:id', async (req, res) => { try { const resourceId = req.params.id; await pangolin_client_1.pangolinClient.deleteResource(resourceId); res.json({ success: true }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // POST /api/pangolin/test-2step — Test 2-step resource + target creation (diagnostic endpoint) router.post('/test-2step', pangolinSetupLimiter, async (req, res) => { try { if (!pangolin_client_1.pangolinClient.configured) { res.status(400).json({ error: { message: 'Pangolin not configured', code: 'NOT_CONFIGURED' }, }); return; } const domain = env_1.env.DOMAIN; const testSubdomain = 'test-2step-' + Math.random().toString(36).substring(7); logger_1.logger.info(`Starting 2-step test for ${testSubdomain}.${domain}`); // Get domain info const domains = await pangolin_client_1.pangolinClient.listDomains(); const matchingDomain = domains.find(d => domain.endsWith(d.baseDomain)); if (!matchingDomain) { res.status(400).json({ error: { message: `Domain not found: ${domain}`, code: 'DOMAIN_NOT_FOUND' }, }); return; } // Get siteId let siteId = env_1.env.PANGOLIN_SITE_ID; if (!siteId) { const sites = await pangolin_client_1.pangolinClient.listSites(); if (sites.length === 0) { res.status(400).json({ error: { message: 'No sites available. Run setup first.', code: 'NO_SITE' }, }); return; } siteId = sites[0].siteId; } const results = { testName: `2-Step Resource + Target (${testSubdomain})`, domainId: matchingDomain.domainId, siteId, steps: [], }; // --- Step 1: Create resource --- let resourceId; try { const createdResource = await pangolin_client_1.pangolinClient.createResource({ name: `Test 2-Step - ${testSubdomain}`, domainId: matchingDomain.domainId, subdomain: testSubdomain, http: true, protocol: 'tcp', }); resourceId = createdResource.resourceId; results.steps.push({ step: 1, name: 'Create HTTP resource', status: 'success', resourceId, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; results.steps.push({ step: 1, name: 'Create HTTP resource', status: 'failed', error: msg, }); res.status(500).json({ success: false, results }); return; } // --- Step 2: Create target (PUT, correct payload) --- try { const targetPayload = { siteId, ip: 'nginx', port: 80, method: 'http', enabled: true, }; const createdTarget = await pangolin_client_1.pangolinClient.createTarget(resourceId, targetPayload); results.steps.push({ step: 2, name: 'Create target', status: 'success', payload: targetPayload, response: createdTarget, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; results.steps.push({ step: 2, name: 'Create target', status: 'failed', error: msg, }); } // --- Step 3: Verify resource --- try { const verified = await pangolin_client_1.pangolinClient.getResource(resourceId); results.steps.push({ step: 3, name: 'Verify resource', status: 'success', resource: verified, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; results.steps.push({ step: 3, name: 'Verify resource', status: 'failed', error: msg, }); } // --- Step 4: Cleanup --- try { await pangolin_client_1.pangolinClient.deleteResource(resourceId); results.steps.push({ step: 4, name: 'Cleanup', status: 'success', }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; results.steps.push({ step: 4, name: 'Cleanup', status: 'failed', error: msg, resourceId, }); } res.json({ success: true, message: '2-step test completed', results }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('2-step test failed:', err); res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // PUT /api/pangolin/resource/:id — Update a resource const updateResourceSchema = zod_1.z.object({ name: zod_1.z.string().max(200).optional(), subdomain: zod_1.z.string().max(200).optional(), ssl: zod_1.z.boolean().optional(), blockAccess: zod_1.z.boolean().optional(), proxyPort: zod_1.z.number().int().optional(), protocol: zod_1.z.string().max(20).optional(), domainId: zod_1.z.string().max(200).optional(), isBaseDomain: zod_1.z.boolean().optional(), http: zod_1.z.boolean().optional(), https: zod_1.z.boolean().optional(), }).passthrough(); router.put('/resource/:id', async (req, res) => { try { const resourceId = req.params.id; const body = updateResourceSchema.parse(req.body); const resource = await pangolin_client_1.pangolinClient.updateResource(resourceId, body); res.json({ success: true, resource }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; logger_1.logger.error('Pangolin resource update failed:', err); res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/certificate/:domainId/:domain — Get certificate info router.get('/certificate/:domainId/:domain', async (req, res) => { try { const { domainId, domain } = req.params; const certificate = await pangolin_client_1.pangolinClient.getCertificate(domainId, domain); res.json({ certificate }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // POST /api/pangolin/certificate/:certId — Update certificate const updateCertificateSchema = zod_1.z.object({ autoRenew: zod_1.z.boolean().optional(), isWildcard: zod_1.z.boolean().optional(), }).passthrough(); router.post('/certificate/:certId', async (req, res) => { try { const certId = req.params.certId; const body = updateCertificateSchema.parse(req.body); const certificate = await pangolin_client_1.pangolinClient.updateCertificate(certId, body); res.json({ success: true, certificate }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); // GET /api/pangolin/resource/:id/clients — List connected clients for a resource router.get('/resource/:id/clients', async (req, res) => { try { const resourceId = req.params.id; const clients = await pangolin_client_1.pangolinClient.listClients(resourceId); res.json({ clients }); } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; res.status(500).json({ error: { message: msg, code: 'PANGOLIN_ERROR' } }); } }); exports.pangolinRouter = router; //# sourceMappingURL=pangolin.routes.js.map