Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions src/routes/api/settings/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion tests/portal_settings_host.test.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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)
Expand Down
Loading