Skip to content
Draft
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
108 changes: 108 additions & 0 deletions assets/js/hubce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"use strict";

// requires newsletter.js
const VERIFY_EMAIL_URL = API_BASE_URL + '/connect/email/verify';
const REFRESH_LICENSE_URL = API_BASE_URL + '/licenses/hub/refresh';

class HubCE {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused class HubCE.

Copilot uses AI. Check for mistakes.

constructor(form, feedbackData, submitData, searchParams) {
this._form = form;
this._feedbackData = feedbackData;
this._submitData = submitData;
this._searchParams = searchParams;
this._submitData.oldLicense = searchParams.get('oldLicense');

// continue after email verified:
if (searchParams.get('verifiedEmail')) {
feedbackData.currentStep = 2;
feedbackData.success = true;
}
}

submit() {
if (this._feedbackData.currentStep === 0) {
this.validateEmail();
} else if (this._feedbackData.currentStep === 1) {
this.sendConfirmationEmail();
} else if (this._feedbackData.currentStep === 2) {
this.getHubLicense();
}
}

validateEmail() {
if (!$(this._form)[0].checkValidity()) {
$(this._form).find(':input').addClass('show-invalid');
this._feedbackData.errorMessage = 'Please fill in all required fields.';
return;
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateEmail() allows progression even when oldLicense is missing (it’s only blocked by the disabled button in the HTML). Because the form can be submitted via Enter key, this path can advance with a null/empty token and later POST it to the backend. Add an explicit oldLicense check here (and surface a clear error) to match the UI constraint.

Suggested change
}
}
// Ensure oldLicense is present before allowing progression.
const oldLicense = this._submitData.oldLicense;
if (!oldLicense) {
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = 'License information is missing. Please open this page using the link provided in your email.';
return;
}

Copilot uses AI. Check for mistakes.
this.onValidationSucceeded();
}

onValidationFailed(error) {
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = error;
}

onValidationSucceeded() {
this._feedbackData.currentStep++;
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = '';
}

sendConfirmationEmail() {
if (!$(this._form)[0].checkValidity()) {
$(this._form).find(':input').addClass('show-invalid');
this._feedbackData.errorMessage = 'Please fill in all required fields.';
return;
}

this._feedbackData.success = false;
this._feedbackData.inProgress = true;
this._feedbackData.errorMessage = '';
$.ajax({
url: VERIFY_EMAIL_URL,
type: 'POST',
data: {
email: this._submitData.email,
oldLicense: this._submitData.oldLicense,
verifyCaptcha: this._submitData.captcha,
verifyEmail: this._submitData.email,
verifyTarget: 'registerhubce'
}
}).done(_ => {
this.onRequestSucceeded();
if (this._submitData.acceptNewsletter) {
subscribeToNewsletter(this._submitData.email, 7); // FIXME move to backend
}
}).fail(xhr => {
this.onRequestFailed(xhr.responseJSON?.message || 'Sending confirmation email failed.');
});
}

getHubLicense() {
$.ajax({
url: REFRESH_LICENSE_URL,
type: 'POST',
data: {
token: this._submitData.oldLicense
}
}).done(response => {
this._feedbackData.licenseText = response;
}).fail(xhr => {
this.onRequestFailed(xhr.responseJSON?.message || 'Fetching license failed.');
});
}
Comment on lines +83 to +95
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHubLicense() doesn’t set inProgress/clear errorMessage, so the UI can’t reliably show loading state or prevent duplicate submits. Also, the template renders an Altcha captcha for the license step, but this request doesn’t send any captcha payload—either remove the captcha from the license step or include its payload in the POST so the widget has an effect.

Copilot uses AI. Check for mistakes.

onRequestFailed(error) {
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = error;
}

onRequestSucceeded() {
this._feedbackData.currentStep++;
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = '';
}

}
5 changes: 5 additions & 0 deletions content/hub-register.de.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "Cryptomator Hub: Registrieren"
url: "/de/hub/register"
type: "hub-register"
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a German page (hub-register.de.html), but there are no German i18n entries for the new hub_ce_registration_* keys (verified: no matches in i18n/de.yaml). The German page will render fallback IDs instead of translations. Add the corresponding i18n/de.yaml entries or avoid publishing the DE page until translations exist.

Suggested change
type: "hub-register"
type: "hub-register"
draft: true

Copilot uses AI. Check for mistakes.
---
5 changes: 5 additions & 0 deletions content/hub-register.en.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "Cryptomator Hub: Register"
url: "/hub/register"
type: "hub-register"
---
29 changes: 29 additions & 0 deletions i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,35 @@
- id: hub_demo_contact_us_button
translation: "Contact Us"

# Hub CE Registration
- id: hub_ce_registration_description
translation: "Register your Cryptomator Hub instance for a free Community Edition license."

- id: hub_ce_registration_steps_title
translation: "Step <span x-text=\"feedbackData.currentStep + 1\"></span> of <span x-text=\"steps.length\"></span>"
- id: hub_ce_registration_steps_next
translation: "Next"

- id: hub_ce_registration_step_1_nav_title
translation: "Email Address"
- id: hub_ce_registration_step_1_title
translation: "What's your email address?"
- id: hub_ce_registration_step_1_email_placeholder
translation: "Email address"

- id: hub_ce_registration_step_2_confirmation_nav_title
translation: "Confirmation"
- id: hub_ce_registration_step_2_confirmation_title
translation: "Please confirm your email address"
- id: hub_ce_registration_step_2_instructions
translation: "We are going to send a confirmation email to <span x-text=\"submitData.email\"></span>"

- id: hub_ce_registration_step_3_license_nav_title
translation: "License Key"
- id: hub_ce_registration_step_3_license_title
translation: "Your Community Edition License Key"


# Hub Managed
- id: hub_managed_description
translation: "Request access to a managed instance of Cryptomator Hub and get your team on board with client-side encryption for your cloud storage."
Expand Down
188 changes: 188 additions & 0 deletions layouts/hub-register/single.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
{{ define "preloads" }}
{{ partial "altcha-css.html" . }}
{{ end }}
{{ define "main" }}
<section x-data="{steps: ['{{ i18n "hub_ce_registration_step_1_nav_title" }}', '{{ i18n "hub_ce_registration_step_2_confirmation_nav_title" }}', '{{ i18n "hub_ce_registration_step_3_license_nav_title" }}'], feedbackData: {currentStep: 0, success: false, inProgress: false, errorMessage: '', licenseText: null}, submitData: {captcha: null, oldLicense: '', email: '', acceptNewsletter: false}, acceptTerms: false, hubCE: null, captchaState: null}" x-init="hubCE = new HubCE($refs.form, feedbackData, submitData, new URLSearchParams(location.hash.substring(1)))" class="container py-12">
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CE registration flow is described as 4 steps (email → accept terms → verify email → get license), but the UI steps array only has 3 steps and there’s no dedicated “verify email” step in the progress indicator. Either align the UI to the described 4-step process or update the workflow/labels to match the implemented behavior.

Copilot uses AI. Check for mistakes.
<header class="mb-6">
<h1 class="font-h1 mb-8">{{ .Title }}</h1>
<p class="lead">{{ i18n "hub_ce_registration_description" }}</p>
</header>


<section class="white-box md:min-h-110 px-4 py-5 md:p-6 md:grid md:grid-cols-3 md:gap-6">
<header class="mb-8 md:col-span-1 md:mt-4">
<nav class="flex items-center justify-center gap-6" aria-label="Progress">
<p class="font-p text-sm text-gray-500 md:hidden">
{{ i18n "hub_ce_registration_steps_title" | safeHTML }}
</p>
<ol role="list" class="flex items-center gap-3 md:flex-col md:items-start md:gap-6">
<template x-for="(step, index) in steps" :key="index">
<li>
<!-- Complete Step -->
<template x-if="index < feedbackData.currentStep &amp;&amp; !feedbackData.success">
<a href="#" class="group" @click.prevent="feedbackData.currentStep = index" :data-umami-event="`hub-managed-nav-step-${index + 1}`">
<span class="flex items-center gap-3">
<div class="relative flex w-5 h-5 shrink-0 items-center justify-center">
<svg class="w-full h-full text-primary group-hover:text-secondary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
</svg>
</div>
<p class="hidden md:block font-p text-sm text-gray-500 group-hover:text-gray-900" x-text="step"></p>
</span>
</a>
</template>
<template x-if="index < feedbackData.currentStep &amp;&amp; feedbackData.success">
<span class="flex items-center gap-3">
<div class="relative flex w-5 h-5 shrink-0 items-center justify-center">
<svg class="w-full h-full text-primary group-hover:text-secondary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
</svg>
</div>
<p class="hidden md:block font-p text-sm text-gray-500 group-hover:text-gray-900" x-text="step"></p>
</span>
</template>

<!-- Current Step -->
<template x-if="index === feedbackData.currentStep">
<div class="flex items-center gap-3" aria-current="step">
<div class="relative flex w-5 h-5 shrink-0 items-center justify-center" aria-hidden="true">
<span class="absolute w-4 h-4 rounded-full bg-primary-l2"></span>
<span class="relative block w-2 h-2 rounded-full bg-primary"></span>
</div>
<p class="hidden md:block font-p text-sm font-medium text-primary" x-text="step"></p>
</div>
</template>

<!-- Upcoming Step -->
<template x-if="index > feedbackData.currentStep">
<div class="flex items-center gap-3">
<div class="relative flex w-5 h-5 shrink-0 items-center justify-center" aria-hidden="true">
<div class="w-2 h-2 rounded-full bg-gray-300"></div>
</div>
<p class="hidden md:block font-p text-sm text-gray-500" x-text="step"></p>
</div>
</template>
</li>
</template>
</ol>
</nav>
</header>

<form x-ref="form" class="md:col-span-2" @submit.prevent="hubCE.submit(); $refs.captcha.reset()">
<!-- Step 1: Email Address -->
Comment on lines +71 to +72
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pressing Enter in the email field (step 1) will submit the form, but $refs.captcha isn’t rendered in that step; calling $refs.captcha.reset() will throw and break the flow. Also, Enter bypasses the disabled “Next” button guard for oldLicense, allowing progression without the required token. Consider guarding the reset (e.g., only when the ref exists) and enforcing oldLicense in validation / making the step-1 navigation button type="button" so the form can’t submit in step 1.

Copilot uses AI. Check for mistakes.
<template x-if="feedbackData.currentStep == 0">
<div class="grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The steps title inside the step content uses hub_managed_steps_title, but this page introduces hub_ce_registration_steps_title. This makes the new CE i18n key unused and risks inconsistent translations between mobile/desktop. Switch these occurrences to the CE-specific key (or remove the CE key if you intend to reuse the managed one).

Suggested change
{{ i18n "hub_managed_steps_title" | safeHTML }}
{{ i18n "hub_ce_registration_steps_title" | safeHTML }}

Copilot uses AI. Check for mistakes.
</p>
<h2 class="font-h2 mb-6">
{{ i18n "hub_ce_registration_step_1_title" }}
</h2>
<input type="email" id="email" class="block input-box w-full mb-8" placeholder="{{ i18n "hub_ce_registration_step_1_email_placeholder" }}" x-init="$el.focus()" x-model="submitData.email" @blur="$el.classList.add('show-invalid')" required>
<div class="mt-auto">
<p :class="{'hidden': !feedbackData.errorMessage}" class="text-sm text-red-600 mb-2" x-text="feedbackData.errorMessage"></p>
<button :disabled="feedbackData.inProgress || !submitData.oldLicense" @click.prevent="hubCE.validateEmail()" class="btn btn-primary w-full md:w-64" data-umami-event="hub-ce-registration-step-1">
<i :class="{'fa-chevron-right': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_ce_registration_steps_next" }}
</button>
</div>
</div>
</div>
</template>

<!-- Step 2: Confirmation -->
<template x-if="feedbackData.currentStep == 1">
<div class="md:col-span-2 grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
</p>
<h2 class="font-h2 mb-6">
{{ i18n "hub_ce_registration_step_2_confirmation_title" }}
</h2>
<p class="font-p mb-6">{{ i18n "hub_ce_registration_step_2_instructions" | safeHTML }}</p>
<p class="font-p text-sm mb-2">
{{ partial "checkbox.html" (dict "context" . "alpineVariable" "acceptTerms" "label" (i18n "accept_hub_managed_terms_and_privacy" | safeHTML)) }}
</p>
<p class="font-p text-sm mb-2">
{{ partial "checkbox.html" (dict "context" . "alpineVariable" "submitData.acceptNewsletter" "label" (i18n "accept_hub_newsletter_optional")) }}
</p>
<div class="mt-auto">
<p :class="{'hidden': !feedbackData.errorMessage}" class="text-sm text-red-600 mb-2" x-text="feedbackData.errorMessage"></p>
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-managed-form" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_managed_step_4_submit" }}
Comment on lines +113 to +115
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This submit button still uses Hub Managed copy/analytics (hub_managed_step_4_submit, hub-managed-form) on the CE registration flow. That will show the wrong label (and mix Umami events). Add CE-specific i18n/event names or reuse the CE ..._steps_next/submit wording consistently.

Suggested change
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-managed-form" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_managed_step_4_submit" }}
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-ce-registration-step-2-submit" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_ce_registration_steps_submit" }}

Copilot uses AI. Check for mistakes.
</button>
{{ $challengeUrl := printf "%s/connect/email/challenge" .Site.Params.apiBaseUrl }}
{{ partial "captcha.html" (dict "challengeUrl" $challengeUrl "captchaPayload" "submitData.captcha" "captchaState" "captchaState") }}
</div>
</div>
</div>
</template>

<!-- Step 3a: License Key (loading...) -->
<template x-if="feedbackData.currentStep == 2 &amp;&amp; !feedbackData.success">
<div class="md:col-span-2 grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
</p>
<h2 class="font-h2 mb-6">
{{ i18n "hub_ce_registration_step_3_license_title" }}
</h2>
<p>Todo: Please check your e-mails.</p>
</div>
</div>
</template>

<!-- Step 3b: License Key (success) -->
<template x-if="feedbackData.currentStep == 2 &amp;&amp; feedbackData.success">
<div class="md:col-span-2 grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
</p>
<h2 class="font-h2 mb-6">
{{ i18n "hub_ce_registration_step_3_license_title" }}
</h2>
<p>Todo: return URL</p>
{{ $challengeUrl := printf "%s/licenses/hub/challenge" .Site.Params.apiBaseUrl }}
{{ partial "captcha.html" (dict "challengeUrl" $challengeUrl "captchaPayload" "submitData.captcha" "captchaState" "captchaState") }}
<textarea class="block input-box w-full h-48 mb-8" x-text="feedbackData.licenseText" readonly></textarea>
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<textarea ... x-text="feedbackData.licenseText"> won’t update the textarea’s value reliably (the project generally uses x-model/:value for textareas). Bind the textarea’s value (e.g., x-model to a readonly field or :value) so the license text updates when the AJAX call completes.

Suggested change
<textarea class="block input-box w-full h-48 mb-8" x-text="feedbackData.licenseText" readonly></textarea>
<textarea class="block input-box w-full h-48 mb-8" :value="feedbackData.licenseText" readonly></textarea>

Copilot uses AI. Check for mistakes.

<div class="mt-auto">
<p :class="{'hidden': !feedbackData.errorMessage}" class="text-sm text-red-600 mb-2" x-text="feedbackData.errorMessage"></p>
<button class="btn btn-primary w-full md:w-64" data-umami-event="hub-ce-registration-step-1">
<i :class="{'fa-chevron-right': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
Todo: Return to Hub
</button>
Comment on lines +134 to +159
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are user-facing placeholder strings (Todo: ...) and a “Return to Hub” button that currently lives inside the form and will submit the form (triggering hubCE.submit()), not actually return anywhere. These placeholders/actions should be implemented (or removed) before merging to avoid shipping incomplete UX and unintended behavior.

Copilot uses AI. Check for mistakes.
</div>
</div>
</div>
</template>
</form>
</section>
</section>
{{ end }}
{{ define "script" }}
{{ if hugo.IsDevelopment }}
{{ $newsletterJs := resources.Get "js/newsletter.js" }}
<script type="text/javascript" src="{{ $newsletterJs.RelPermalink }}" defer></script>
{{ $hubCeJs := resources.Get "js/hubce.js" }}
<script type="text/javascript" src="{{ $hubCeJs.RelPermalink }}" defer></script>
{{ $altchaJs := resources.Get "js/altcha/altcha.js" }}
<script type="module" src="{{ $altchaJs.RelPermalink }}" defer></script>
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" }}
<script type="module" src="{{ $altchaWorkerJs.RelPermalink }}" defer></script>
{{ else }}
{{ $newsletterJs := resources.Get "js/newsletter.js" | minify | fingerprint }}
<script type="text/javascript" src="{{ $newsletterJs.RelPermalink }}" integrity="{{ $newsletterJs.Data.Integrity }}" defer></script>
{{ $hubCeJs := resources.Get "js/hubce.js" | minify | fingerprint }}
<script type="text/javascript" src="{{ $hubCeJs.RelPermalink }}" integrity="{{ $hubCeJs.Data.Integrity }}" defer></script>
{{ $altchaJs := resources.Get "js/altcha/altcha.js" | minify | fingerprint }}
<script type="module" src="{{ $altchaJs.RelPermalink }}" integrity="{{ $altchaJs.Data.Integrity }}" defer></script>
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" }}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production, altchaWorkerJs isn’t minify|fingerprint’d but an integrity attribute is still emitted. This will likely produce an empty/incorrect integrity value and prevents cache-busting. Apply the same minify | fingerprint pipeline as the other scripts (or drop integrity if you intentionally don’t fingerprint).

Suggested change
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" }}
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" | minify | fingerprint }}

Copilot uses AI. Check for mistakes.
<script type="module" src="{{ $altchaWorkerJs.RelPermalink }}" integrity="{{ $altchaWorkerJs.Data.Integrity }}" defer></script>
{{ end }}
{{ end }}
Loading