Compare commits

..

10 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 6b342b0b8d feat: create wildcard CNAME rrset after new deSEC account domain registration
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/2390d12f-4776-4f9a-8382-c10f090dadcb

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 00:03:59 +00:00
Simon L. d58a34b605 Revert "feat: show deSEC password field only after email-already-registered failure, via POST /containers and Twig (no sessions, no query params)"
This reverts commit 44b257a2b5.
2026-04-26 18:37:19 +02:00
copilot-swe-agent[bot] 44b257a2b5 feat: show deSEC password field only after email-already-registered failure, via POST /containers and Twig (no sessions, no query params)
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/9eadc186-a642-409b-871d-f2bbb47f20ce

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-26 16:36:06 +00:00
Simon L. 2b78dcc9cc Revert "feat: hide deSEC password field until email-already-registered 422 error"
This reverts commit f0fb065dc2.
2026-04-26 18:25:43 +02:00
Simon L. 11d8050085 Revert "refactor: move deSEC password-reveal logic from JS to Twig (PRG pattern)"
This reverts commit 1c6ca098d5.
2026-04-26 18:25:39 +02:00
copilot-swe-agent[bot] 1c6ca098d5 refactor: move deSEC password-reveal logic from JS to Twig (PRG pattern)
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/159fc9de-4eb7-4131-8dee-9166045156e6

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-25 16:43:49 +00:00
copilot-swe-agent[bot] 5343353bb5 feat: clear manually-entered deSEC password after successful login
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/902f3119-a4ee-4fa5-8865-510513cc4046

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-25 16:35:22 +00:00
copilot-swe-agent[bot] f0fb065dc2 feat: hide deSEC password field until email-already-registered 422 error
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/d4f48d74-6e53-474c-b5bf-a9705525de45

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-25 16:32:31 +00:00
copilot-swe-agent[bot] b71afd933b refactor: extract deSEC registration form into includes/desec-register.twig
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/5799217f-4a9b-4e23-9f0f-4bd0d37999b2

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-25 16:26:23 +00:00
copilot-swe-agent[bot] 24f8a126cb feat: allow using existing deSEC account by supplying a password
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/57f233da-439b-4992-888c-82fad2dfa2cc

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-25 16:21:46 +00:00
4 changed files with 119 additions and 39 deletions
+4 -3
View File
@@ -15,9 +15,10 @@ readonly class DesecController {
public function Register(Request $request, Response $response, array $args): Response {
try {
$email = (string)($request->getParsedBody()['desec_email'] ?? '');
$slug = (string)($request->getParsedBody()['desec_slug'] ?? '');
$this->desecManager->register($email, $slug);
$email = (string)($request->getParsedBody()['desec_email'] ?? '');
$slug = (string)($request->getParsedBody()['desec_slug'] ?? '');
$password = (string)($request->getParsedBody()['desec_password'] ?? '');
$this->desecManager->register($email, $slug, $password);
return $response->withStatus(201)->withHeader('Location', '.');
} catch (\Exception $ex) {
$response->getBody()->write($ex->getMessage());
+88 -12
View File
@@ -29,9 +29,14 @@ class DesecManager {
* Full registration flow: validates inputs, creates an account if needed,
* registers the domain, enables required containers, and updates the DNS record.
*
* When $password is non-empty the user is logging into an existing deSEC account
* rather than creating a new one. When $password is empty a new account is created
* with a randomly generated password (unless an account was already registered in a
* previous attempt).
*
* @throws \Exception on any validation or API error
*/
public function register(string $email, string $slug): void {
public function register(string $email, string $slug, string $password = ''): void {
if ($this->configurationManager->domain !== '') {
throw new \Exception('A domain is already configured. Reset the AIO instance first to register a new domain.');
}
@@ -41,22 +46,34 @@ class DesecManager {
? $this->configurationManager->desecToken
: null;
$validatedEmail = null;
if (!$accountAlreadyRegistered) {
$validatedEmail = $this->validateEmail($email);
}
$validatedSlug = $this->validateSlug($slug);
if (!$accountAlreadyRegistered) {
// 24 random bytes → 48-char hex password; satisfies deSEC's minimum length
// and lets the user log in at desec.io if they ever need to.
$password = bin2hex(random_bytes(24));
$token = $this->registerAccount($validatedEmail, $password);
$this->saveAccountCredentials($token, $password, $validatedEmail);
$validatedEmail = $this->validateEmail($email);
$validatedPassword = trim($password);
if ($validatedPassword !== '') {
// The user supplied their existing deSEC password — log in instead of registering.
// Store an empty password: the token is all we need; the user's password must not be persisted.
$token = $this->loginAccount($validatedEmail, $validatedPassword);
$this->saveAccountCredentials($token, '', $validatedEmail);
} else {
// 24 random bytes → 48-char hex password; satisfies deSEC's minimum length
// and lets the user log in at desec.io if they ever need to.
$generatedPassword = bin2hex(random_bytes(24));
$token = $this->registerAccount($validatedEmail, $generatedPassword);
$this->saveAccountCredentials($token, $generatedPassword, $validatedEmail);
}
}
$isNewAccount = !$accountAlreadyRegistered && trim($password) === '';
$domain = $this->registerDomain($token, $validatedSlug);
if ($isNewAccount) {
$this->createWildcardCname($token, $domain);
}
$this->enableDesecContainers();
$this->configurationManager->setDomain($domain, true);
$this->updateIpIfDesecDomain();
@@ -114,7 +131,7 @@ class DesecManager {
if (is_array($data) && isset($data['email'])) {
throw new \Exception(
'This email address is already registered at deSEC. '
. 'Please log in at https://desec.io to retrieve your token and set up your domain manually.',
. 'If this is your account, please enter your deSEC password in the password field and try again.',
);
}
throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body);
@@ -132,6 +149,39 @@ class DesecManager {
return $data['token']['token'];
}
/**
* Authenticates with an existing deSEC account and returns the API token issued for it.
*
* @throws \Exception on invalid credentials, network failure, or an unexpected HTTP response
*/
public function loginAccount(string $email, string $password): string {
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/auth/login/', [
'json' => ['email' => $email, 'password' => $password],
]);
} catch (TransferException $e) {
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
}
$code = $res->getStatusCode();
$body = $res->getBody()->getContents();
if ($code === 400 || $code === 403) {
throw new \Exception('Could not log in to deSEC: invalid email address or password.');
}
if ($code !== 200 && $code !== 201) {
throw new \Exception('Unexpected response from deSEC during login (HTTP ' . $code . '): ' . $body);
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data) || !isset($data['token']) || !is_string($data['token'])) {
throw new \Exception('Could not extract the API token from the deSEC login response. Please try again.');
}
return $data['token'];
}
/**
* Registers a dedyn.io domain for the authenticated account.
* When $slug is empty a random 10-character slug is tried up to MAX_SLUG_ATTEMPTS times.
@@ -174,6 +224,32 @@ class DesecManager {
throw new \Exception('Could not register a free dedyn.io domain after ' . self::MAX_SLUG_ATTEMPTS . ' attempts. Please try again.');
}
/**
* Creates a wildcard CNAME rrset (*.domain → domain.) for a newly registered domain.
* Errors are logged but do not abort the overall registration.
*/
private function createWildcardCname(string $token, string $domain): void {
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/domains/' . $domain . '/rrsets/', [
'headers' => ['Authorization' => 'Token ' . $token],
'json' => [
'subname' => '*',
'type' => 'CNAME',
'ttl' => 3600,
'records' => [$domain . '.'],
],
]);
} catch (TransferException $e) {
error_log('Could not create wildcard CNAME for ' . $domain . ': ' . $e->getMessage());
return;
}
$code = $res->getStatusCode();
if ($code !== 201) {
error_log('Unexpected response when creating wildcard CNAME for ' . $domain . ' (HTTP ' . $code . '): ' . $res->getBody()->getContents());
}
}
/**
* Persists deSEC account credentials to the AIO configuration atomically.
*/
+1 -24
View File
@@ -132,30 +132,7 @@
{% endif %}
<p><strong>Hint:</strong> If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following <a target="_blank" href="https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation">this documentation</a>.</p>
</details>
<details{% if desec_account_registered %} open{% endif %}>
<summary>Don't have a domain? Get a free one from deSEC</summary>
<p><a target="_blank" href="https://desec.io">deSEC</a> offers free dynamic DNS subdomains under <strong>dedyn.io</strong>. AIO can register an account and a subdomain for you automatically. The <strong>caddy</strong> community container will be enabled as a reverse proxy, the <strong>dnsmasq</strong> container will be enabled for local DNS resolution, and the mastercontainer will keep your DNS record up to date automatically.</p>
{% if desec_account_registered %}
<p>Your deSEC account (<strong>{{ desec_email }}</strong>) was registered successfully but the domain could not be registered. Please enter a desired subdomain slug (the part before <code>.dedyn.io</code>) and try again, or leave it blank for a random one.</p>
<p>Your deSEC login credentials (for <a target="_blank" href="https://desec.io">desec.io</a>): Email: <strong>{{ desec_email }}</strong>. <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details>. Please save these in a safe place.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
{% else %}
<p>Please enter your email address. You can also enter a desired subdomain slug (the part before <code>.dedyn.io</code>); leave it blank for a random one.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="email" name="desec_email" placeholder="your@email.com" required />
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
<p><strong>Note:</strong> By submitting this form you agree to the <a target="_blank" href="https://desec.io/terms">deSEC terms of service</a>. The registered domain and your deSEC account credentials are stored in the AIO configuration. After registration, set your router's DHCP DNS server to this machine's local IP address so LAN devices resolve the domain locally (see the <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq">dnsmasq documentation</a>). Alternatively adjust the hosts files on your clients so that they can reach the server using the local ip-address.</p>
{% endif %}
</details>
{% include 'includes/desec-register.twig' %}
{% endif %}
<h2>Restore former AIO instance from backup</h2>
@@ -0,0 +1,26 @@
<details{% if desec_account_registered %} open{% endif %}>
<summary>Don't have a domain? Get a free one from deSEC</summary>
<p><a target="_blank" href="https://desec.io">deSEC</a> offers free dynamic DNS subdomains under <strong>dedyn.io</strong>. AIO can register an account and a subdomain for you automatically. The <strong>caddy</strong> community container will be enabled as a reverse proxy, the <strong>dnsmasq</strong> container will be enabled for local DNS resolution, and the mastercontainer will keep your DNS record up to date automatically.</p>
{% if desec_account_registered %}
<p>Your deSEC account (<strong>{{ desec_email }}</strong>) was registered successfully but the domain could not be registered. Please enter a desired subdomain slug (the part before <code>.dedyn.io</code>) and try again, or leave it blank for a random one.</p>
<p>Your deSEC login credentials (for <a target="_blank" href="https://desec.io">desec.io</a>): Email: <strong>{{ desec_email }}</strong>. <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details>. Please save these in a safe place.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
{% else %}
<p>Please enter your email address. You can also enter a desired subdomain slug (the part before <code>.dedyn.io</code>); leave it blank for a random one.</p>
<p>If you already have a deSEC account for this email address, enter your deSEC password in the optional password field below to log in with it instead of creating a new account.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="email" name="desec_email" placeholder="your@email.com" required />
<input type="password" name="desec_password" placeholder="deSEC password (only if already registered)" autocomplete="current-password" />
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
<p><strong>Note:</strong> By submitting this form you agree to the <a target="_blank" href="https://desec.io/terms">deSEC terms of service</a>. The registered domain and your deSEC account credentials are stored in the AIO configuration. After registration, set your router's DHCP DNS server to this machine's local IP address so LAN devices resolve the domain locally (see the <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq">dnsmasq documentation</a>). Alternatively adjust the hosts files on your clients so that they can reach the server using the local ip-address.</p>
{% endif %}
</details>