diff --git a/dashcaddy-api/__tests__/tailscale.test.js b/dashcaddy-api/__tests__/tailscale.test.js index 6d7a290..6055139 100644 --- a/dashcaddy-api/__tests__/tailscale.test.js +++ b/dashcaddy-api/__tests__/tailscale.test.js @@ -2,12 +2,16 @@ * Tailscale Route Tests * * Tests Tailscale status, configuration, and connection-checking endpoints. - * The Tailscale routes are mounted without a prefix on the API router, so: - * - GET /api/status — Tailscale status (returns null status if not installed) - * - POST /api/config — NOTE: shadowed by config/settings.js which also defines POST /config; - * we test it here but it may hit the DashCaddy config route instead. - * - GET /api/check-connection — Check if request comes from Tailscale IP - * - POST /api/tailscale/oauth-config — OAuth credential setup (requires live API) + * The Tailscale routes are mounted at /api/tailscale/ on the API router: + * - GET /api/tailscale/status — Tailscale status + * - POST /api/tailscale/config — Update Tailscale configuration + * - GET /api/tailscale/check-connection — Check if request comes from Tailscale IP + * - GET /api/tailscale/devices — List Tailscale devices + * - POST /api/tailscale/protect-service — Toggle Tailscale-only for a service + * - POST /api/tailscale/oauth-config — OAuth credential setup (requires live API) + * - GET /api/tailscale/api-devices — Enriched device list from API + * - POST /api/tailscale/sync — Trigger API sync + * - GET /api/tailscale/acl — Fetch ACL policy */ const request = require('supertest'); @@ -34,9 +38,9 @@ describe('Tailscale Routes', () => { try { fs.unlinkSync(testConfigFile); } catch (e) { /* ignore */ } }); - describe('GET /api/status (Tailscale status)', () => { + describe('GET /api/tailscale/status', () => { test('should return 200 with status data', async () => { - const res = await request(app).get('/api/status'); + const res = await request(app).get('/api/tailscale/status'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); @@ -57,9 +61,9 @@ describe('Tailscale Routes', () => { }); }); - describe('GET /api/check-connection', () => { + describe('GET /api/tailscale/check-connection', () => { test('should return 200 with connection info', async () => { - const res = await request(app).get('/api/check-connection'); + const res = await request(app).get('/api/tailscale/check-connection'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); @@ -69,7 +73,7 @@ describe('Tailscale Routes', () => { }); test('should detect non-Tailscale IP for localhost requests', async () => { - const res = await request(app).get('/api/check-connection'); + const res = await request(app).get('/api/tailscale/check-connection'); expect(res.statusCode).toBe(200); // Supertest connects via loopback, not a 100.x.x.x address @@ -77,9 +81,9 @@ describe('Tailscale Routes', () => { }); }); - describe('GET /api/devices (Tailscale devices)', () => { + describe('GET /api/tailscale/devices', () => { test('should return 200 with devices array', async () => { - const res = await request(app).get('/api/devices'); + const res = await request(app).get('/api/tailscale/devices'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); @@ -122,10 +126,10 @@ describe('Tailscale Routes', () => { }); }); - describe('POST /api/protect-service', () => { + describe('POST /api/tailscale/protect-service', () => { test('should reject missing subdomain', async () => { const res = await request(app) - .post('/api/protect-service') + .post('/api/tailscale/protect-service') .send({}); expect(res.statusCode).toBe(400); diff --git a/dashcaddy-api/health-checker.js b/dashcaddy-api/health-checker.js index 200a27a..6327d26 100644 --- a/dashcaddy-api/health-checker.js +++ b/dashcaddy-api/health-checker.js @@ -168,7 +168,7 @@ class HealthChecker extends EventEmitter { port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method, - timeout: config.timeout || 10000, + timeout: config.timeout || 20000, headers: config.headers || {}, rejectUnauthorized: false // Trust internal CA certs (.sami TLD) }; @@ -497,7 +497,7 @@ class HealthChecker extends EventEmitter { name: config.name || serviceId, url: config.url, method: config.method || 'GET', - timeout: config.timeout || 10000, + timeout: config.timeout || 20000, expectedStatusCodes: config.expectedStatusCodes || [200], expectedBodyPattern: config.expectedBodyPattern, expectedBodyContains: config.expectedBodyContains, diff --git a/dashcaddy-api/routes/tailscale.js b/dashcaddy-api/routes/tailscale.js index b898a72..07b807b 100644 --- a/dashcaddy-api/routes/tailscale.js +++ b/dashcaddy-api/routes/tailscale.js @@ -177,7 +177,7 @@ module.exports = function(ctx) { // ── Tailscale API Integration (OAuth 2.0) ── // Save OAuth client credentials + validate by exchanging for a token - router.post('/tailscale/oauth-config', ctx.asyncHandler(async (req, res) => { + router.post('/oauth-config', ctx.asyncHandler(async (req, res) => { const { clientId, clientSecret, tailnet } = req.body; if (!clientId || !clientSecret || !tailnet) { @@ -235,7 +235,7 @@ module.exports = function(ctx) { }, 'tailscale-oauth-config')); // Remove OAuth credentials and disable API sync - router.delete('/tailscale/oauth-config', ctx.asyncHandler(async (req, res) => { + router.delete('/oauth-config', ctx.asyncHandler(async (req, res) => { await ctx.credentialManager.delete('tailscale.oauth.client_id'); await ctx.credentialManager.delete('tailscale.oauth.client_secret'); @@ -250,7 +250,7 @@ module.exports = function(ctx) { }, 'tailscale-oauth-delete')); // Get enriched device list from Tailscale API - router.get('/tailscale/api-devices', ctx.asyncHandler(async (req, res) => { + router.get('/api-devices', ctx.asyncHandler(async (req, res) => { if (!ctx.tailscale.config.oauthConfigured) { return ctx.errorResponse(res, 400, 'Tailscale API not configured. Set up OAuth first.'); } @@ -264,7 +264,7 @@ module.exports = function(ctx) { }, 'tailscale-api-devices')); // Manually trigger an API sync - router.post('/tailscale/sync', ctx.asyncHandler(async (req, res) => { + router.post('/sync', ctx.asyncHandler(async (req, res) => { if (!ctx.tailscale.config.oauthConfigured) { return ctx.errorResponse(res, 400, 'Tailscale API not configured. Set up OAuth first.'); } @@ -279,7 +279,7 @@ module.exports = function(ctx) { }, 'tailscale-sync')); // Fetch ACL policy (read-only) - router.get('/tailscale/acl', ctx.asyncHandler(async (req, res) => { + router.get('/acl', ctx.asyncHandler(async (req, res) => { const token = await ctx.tailscale.getAccessToken(); const tailnet = ctx.tailscale.config.tailnet; if (!token || !tailnet) { diff --git a/dashcaddy-api/server.js b/dashcaddy-api/server.js index d20456d..64ec6e4 100644 --- a/dashcaddy-api/server.js +++ b/dashcaddy-api/server.js @@ -1186,7 +1186,7 @@ apiRouter.use(serviceRoutes(ctx)); apiRouter.use(healthRoutes(ctx)); apiRouter.use(monitoringRoutes(ctx)); apiRouter.use(updatesRoutes(ctx)); -apiRouter.use(tailscaleRoutes(ctx)); +apiRouter.use('/tailscale', tailscaleRoutes(ctx)); apiRouter.use(sitesRoutes(ctx)); apiRouter.use(credentialsRoutes(ctx)); apiRouter.use(arrRoutes(ctx));