diff --git a/src/i18n/de.json b/src/i18n/de.json index 70d20aac..5826ee25 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -1967,7 +1967,6 @@ "settings.portal.host_not_verified": "Domain ist nicht verifiziert", "settings.portal.host_invalid_prefix": "Ungültiges Subdomain-Präfix", "settings.portal.host_collision": "Host kollidiert mit der GateControl-Adresse, einer Route-Domain oder einem Peer-Hostnamen", - "settings.portal.host_requires_caddy_email": "Eine öffentliche Domain benötigt eine konfigurierte ACME-E-Mail (GC_CADDY_EMAIL) für die Zertifikatsausstellung", "settings.portal.address": "Portal-Adresse", "settings.portal.base_domain": "Basis-Domain", "settings.portal.prefix": "Subdomain-Präfix", diff --git a/src/i18n/en.json b/src/i18n/en.json index cc94f70b..b7b078a9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1967,7 +1967,6 @@ "settings.portal.host_not_verified": "Domain is not verified", "settings.portal.host_invalid_prefix": "Invalid subdomain prefix", "settings.portal.host_collision": "Host collides with the GateControl address, a route domain, or a peer hostname", - "settings.portal.host_requires_caddy_email": "A public domain needs an ACME email (GC_CADDY_EMAIL) configured for certificate issuance", "settings.portal.address": "Portal address", "settings.portal.base_domain": "Base domain", "settings.portal.prefix": "Subdomain prefix", diff --git a/src/routes/api/settings/portal.js b/src/routes/api/settings/portal.js index bbd160bc..bf1c8a54 100644 --- a/src/routes/api/settings/portal.js +++ b/src/routes/api/settings/portal.js @@ -60,11 +60,11 @@ router.put('/portal', (req, res) => { const prefix = String(body.prefix !== undefined ? (body.prefix == null ? '' : body.prefix) : settings.get('portal.prefix', 'home')).trim().toLowerCase(); const v = validatePortalHost(base, prefix); if (!v.ok) return res.status(400).json({ ok: false, error: req.t('settings.portal.host_' + v.error) }); - // Preflight: a public host needs an ACME email, else Caddy requests no cert and the - // feature silently fails its core value. Reject before persisting/syncing. - if (base && !config.caddy.email) { - return res.status(400).json({ ok: false, error: req.t('settings.portal.host_requires_caddy_email') }); - } + // NOTE: GC_CADDY_EMAIL is intentionally NOT required. ACME issuance (Let's Encrypt) + // does not need an account email; when GC_CADDY_EMAIL is empty, buildTlsAutomation + // emits no explicit tls policy and Caddy's default automation obtains a public cert + // for the portal host exactly as it already does for every route domain. An email is + // optional (only used for LE expiry notices, which Caddy's auto-renew makes moot). settings.set('portal.base_domain', base); settings.set('portal.prefix', prefix); // ALWAYS re-sync when a host field was submitted + validated — NO "only when changed" diff --git a/tests/portal_settings_host.test.js b/tests/portal_settings_host.test.js index 8f75f2aa..ad876de8 100644 --- a/tests/portal_settings_host.test.js +++ b/tests/portal_settings_host.test.js @@ -1,7 +1,9 @@ 'use strict'; const crypto = require('crypto'); process.env.GC_ENCRYPTION_KEY = process.env.GC_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex'); -process.env.GC_CADDY_EMAIL = process.env.GC_CADDY_EMAIL || 'test@example.com'; +// NOTE: GC_CADDY_EMAIL is deliberately NOT set here. The portal must accept a verified +// public domain even with no ACME email configured (Let's Encrypt issuance needs none); +// these tests run with config.caddy.email === '' to prove that. const { test, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, getAgent, getCsrf } = require('./helpers/setup'); @@ -21,6 +23,16 @@ test('PUT accepts a verified base domain + prefix and GET reflects it', async () assert.equal(get.body.data.isPublic, true); }); +test('PUT accepts a verified public domain with NO GC_CADDY_EMAIL configured (ACME needs no account email)', async () => { + assert.equal(require('../config/default').caddy.email, '', 'precondition: caddy email is unset for this test'); + getDb().prepare("INSERT INTO domains (domain, status) VALUES ('domaincaster.com','verified')").run(); + const agent = getAgent(); const csrf = getCsrf(); + await agent.put('/api/v1/settings/portal').set('X-CSRF-Token', csrf) + .send({ base_domain: 'domaincaster.com', prefix: 'home' }).expect(200); + const get = await agent.get('/api/v1/settings/portal').expect(200); + assert.equal(get.body.data.isPublic, true); +}); + test('PUT rejects an unverified base domain (400)', async () => { const agent = getAgent(); const csrf = getCsrf(); await agent.put('/api/v1/settings/portal').set('X-CSRF-Token', csrf)