3 Commits (9f356ad923da54a9d3c8fa12cd414ec8a9fd094a)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
507636f38f |
Die abgebuchte Domain wird jetzt wirklich von der Maschine genommen
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Der Registereintrag nannte den falschen Grund: das Deaktivieren startet sehr wohl eine Provisionierung. CustomDomainAccess::deactivate() ruft seit Langem ReapplyInstanceAddress, das legt einen Lauf der `address`-Pipeline an und schickt AdvanceRunJob auf die provisioning-Warteschlange; ConfigureNextcloud loescht dort trusted_domains 2 und ConfigureDnsAndTls schreibt den Router ohne den Namen neu. Das ist gebaut und geprueft. Der Schaden war trotzdem echt, nur eine Tuer weiter. Erreicht wurde deactivate() allein ueber PlanChange::settleCustomDomain, also ueber den Paketwechsel. Der zweite und haeufigere Weg, auf dem das Recht endet — der Kunde bucht das Modul in der Abrechnung ab, clupilot:end-cancelled-addons haelt den Termin am Ende des bezahlten Zeitraums — ging an dieser Stelle vorbei: BookAddon::cancel() lieferte Speicher nach und sprach mit Stripe, fragte aber niemanden nach der Adresse. Die Domain verschwand aus jeder Ansicht und blieb auf der Maschine stehen. BookAddon::cancel() fragt jetzt CustomDomainAccess::enforce() — die ganze Regel, nicht den Modulschluessel: wer von Team auf Business aufgestuft hat und sein altes Modul loswird, behaelt die Domain, weil das Paket sie selbst traegt. Und der Anstoss darf die Entscheidung nicht kippen. deactivate() faengt jetzt einen Fehlschlag der Nachfuehrung ab und schreibt ihn als Fehler ins Log: die Wahrheit steht in der Datenbank, die Maschine zieht nach, und eine Kuendigung haengt nicht daran, ob ein fremder Host gerade antwortet. Die Gegenrichtung brauchte nichts: der Entzug loescht die Domain-Spalte, also traegt der Kunde sie nach der Neubuchung neu ein und weist sie neu nach — und genau dort haengt seit jeher der Lauf, der sie wieder ausliefert. Ein Test haelt das fest, damit es keine Einbahnstrasse wird. Registereintrag gestrichen. Rot gesehen: ohne den settleCustomDomain-Aufruf fallen drei der vier neuen Tests; ohne das try/catch faellt der vierte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
712803edd6 |
Serve the custom domain, not just announce it
A verified custom domain was reported as the customer's address by
Instance::address(), by the portal and by the credentials mail while
nothing on the platform routed it: the Traefik router's rule was
hard-coded to {subdomain}.{zone}. The address a customer was handed
answered nothing, and a withdrawn domain stayed in Nextcloud's
trusted_domains forever, because the only thing that ever wrote either
was the initial provisioning run.
- TraefikWriter::write() takes a LIST of hostnames under one stable
router name — the platform address always, the verified domain as
well when there is one. One file per instance, so a withdrawal is a
rewrite rather than a second thing somebody has to remember.
- ConfigureDnsAndTls records the custom domain's certificate instead of
waiting on it: it depends on an A record in the customer's own zone,
which may never appear, and must never fail a run. The platform
address keeps its 840s deadline. instances.domain_cert_ok tells
"proven" apart from "answering", and the portal now says which.
- ConfigureNextcloud deletes trusted_domains 2 when there is no verified
domain, so a withdrawn one stops being trusted.
- New `address` pipeline (those two steps) plus ReapplyInstanceAddress,
which starts one against the order and refuses to start a second while
any run is in flight. The route is rewritten when the hostname list
differs from what the router carries — not on route_written, which
would short-circuit exactly the case a re-apply exists for.
- Triggered where the address changes: verification flipping either way
in clupilot:verify-domains, the customer's own domain page, and
CustomDomainAccess::deactivate() on a package downgrade.
- A maintenance run no longer condemns its subject: an address run that
failed used to mark the order failed and release the live instance
with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
b3651a14be |
Decide who may have their own domain, and decide it in one place
tests / pest (push) Failing after 9m46s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
The domain page shipped with no access control at all: the `custom_domain`
plan feature was checked nowhere in the codebase, and every customer on
every package could point a domain at their instance. This is the owner's
rule, built as one authority that everything else asks.
Start impossible — not bookable, not upgradable, not offered
Team optional, via the `custom_domain` module
Business included, because the plan version carries the feature
Enterprise included, likewise
App\Services\Billing\CustomDomainAccess answers all of it: may this
contract use one, may it book the module, why not in words a customer can
be shown, what a downgrade would do to a domain they have, and — after a
change lands — making the state match. Read against the FROZEN plan
version, never today's catalogue, so a feature added to a package later
cannot reach into a contract signed before it.
Which packages cannot have one at all is an explicit list in
config/provisioning.php, beside the module's price. The catalogue cannot
answer it: plan versions model what a package HAS, and nothing models what
a package may BUY. Both available proxies are dishonest — "the lowest tier
on sale" hands the right to a grandfathered Start customer the day the
owner stops selling Start or adds something cheaper below it, and reading
it off `branding` would sell a domain to anyone allowed to upload a logo.
The config comment says so.
Enforced where it can be enforced today:
- BookAddon refuses the module, with the sentence the customer is shown.
GrantAddon now writes its synthetic order inside the transaction, so a
refusal cannot leave a paid order for a module nobody got.
- The portal drops the card where it cannot be booked and shows it as
included where the package carries it — no price, no button.
- Billing::purchase() re-checks, because a hidden button is markup.
- DowngradeCheck reports the consequence beside the blockers, so the
customer reads it above the button rather than after it: losing the
domain (Team -> Start), or the choice (Business -> Team, where keeping
it means booking the module).
- PlanChange::settleCustomDomain() applies it once a change lands: the
module stops being charged for and the instance goes back to its
platform address. It clears the whole domain, not only the verified
flag — clupilot:verify-domains re-reads every row that still has a
domain and a token, and would switch it back on the next night.
- The price sheet's own-domain row now reads dash / optional + price /
included, from the same authority; the three cell states already
existed and the view is unchanged.
STILL TO APPLY, ONCE THIS AND THE CUSTOM-DOMAIN BRANCH MEET — one line at
the top of App\Livewire\CustomDomain::mount(), which belongs to the other
session and is deliberately untouched here:
abort_unless(app(\App\Services\Billing\CustomDomainAccess::class)->allowsCustomer($this->customer()), 403);
routes/web.php is left alone for the same reason. The mount() guard covers
the route and the component both; a middleware would only repeat it.
Follow-ups, named rather than built:
- Nothing applies a plan change anywhere yet, so settleCustomDomain()
has no caller. Whoever builds that must call it after the new contract
opens, and must decide whether a booked module is carried onto the new
contract — enforce() cancels the booking on the contract it is given.
- Deactivation sets the state; it does not re-run provisioning. Traefik
and Nextcloud's trusted_domains still name the old address until
ConfigureDnsAndTls / ConfigureNextcloud run again.
- App\Support\Navigation still shows the Domain tab to every customer;
it can ask allowsCustomer() the same way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|