Commit Graph

8 Commits (v1.3.62)

Author SHA1 Message Date
nexxo 3a4324fb6f Give people a way back in, and put the URL in English
tests / pest (push) Failing after 7m54s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
── The hole ────────────────────────────────────────────────────────────────
There was no password reset. Fortify's feature was commented out, so there
was no link on the sign-in form, no page and no route — a customer who forgot
their password was locked out of their own cloud until somebody opened a
shell. On a product whose selling point is that you can ring somebody, that
is a support call a week and an embarrassing one.

Two pages of ours under Fortify's route names (R1/R2), because with views off
Fortify registers only the POST endpoints. The mail is a Mailable in this
product's design rather than the framework's MailMessage: somebody who has
just been locked out is exactly the person a phishing mail is aimed at, and a
message that looks nothing like the rest of our post is one they cannot check
— ours carries the footer that names our domains.

Three decisions worth stating, each with a test:

The answer is identical whether the address is known or not. "No account with
that address" turns the form into a way of finding out who is a customer.

The reset kills every other session. Whoever knew the old password may still
be signed in somewhere, and a reset that leaves them there has fixed nothing.

It does NOT sign the visitor in. The link arrived by email, and a mailbox
somebody else can read would otherwise be a session somebody else gets.

── /sicherheit → /security ─────────────────────────────────────────────────
R13: paths are English. Mine was not. The old path stays as a permanent
redirect because it has already gone out in mail footers.

── One wordmark ────────────────────────────────────────────────────────────
"Sometimes it says Cloud and sometimes it does not" — correct. It was on the
footer, the placeholder and the maintenance screen, and absent from the
header, the sidebar and the sign-in plate. The product is called CluPilot
Cloud; that is the name on the invoices and in every mail, so it is now the
name everywhere. The console sidebar takes the small size so the lockup, the
ADMIN badge and the close button still share one line (R18).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:50:29 +02:00
nexxo 21762284a7 Bind the portal to its own hostname too, so the website cannot serve it
tests / pest (push) Failing after 7m48s Details
tests / assets (push) Successful in 23s Details
tests / release (push) Has been skipped Details
The previous release bound the landing page and stopped there. The website duly
vanished from the portal — and the portal stayed reachable from the website:
www.clupilot.com/dashboard, /login, /settings all kept working. Half a
separation is no separation, and it was reported back within the hour.

APP_HOST binds every portal route, Fortify's own POST actions included through
fortify.domain — a sign-in form on the website's hostname would otherwise post
to a route that answers there. SITE_HOST now takes the legal pages and
robots.txt with the landing page rather than leaving them everywhere; binding
them is also what makes route('legal.impressum') produce a www URL from inside
a queued mail, where there is no request to take a hostname from.

There is no clever middleware here and there should not be. Route::domain() is
the mechanism: a route registered for one host does not exist on another, and a
request for it gets the 404 it deserves. The only deliberate exception is "/" on
the portal host, which redirects to the dashboard or the sign-in page — that is
what people type from memory, and turning it into an error page to make a point
about hostnames helps nobody. The Stripe webhook and /up stay host-agnostic:
Stripe posts to whichever URL it was given, and a mismatch there loses payments.

Both are opt-in and empty by default, which every development machine reached
by a bare IP depends on.

The tests now check BOTH directions for every path rather than only the one
that was reported. That is the mistake this commit exists to correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:06:50 +02:00
nexxo 5b8c28595a Require a confirmed address before an account can use anything
Registration opened an account that could sign in immediately on an address
nobody had proved was theirs — and anybody can type a stranger's into a
registration form. Both halves were commented out: the Fortify feature and
MustVerifyEmail on the model.

`verified` sits on the whole portal group rather than on the pages that spend
money. An unconfirmed address is not a billing problem to be caught at
checkout; it is an account that may not belong to the person holding it, and
the servers, the users and the backups behind it are worth as much to whoever
typed the address as to whoever owns it.

The mail is a Mailable in this product's design rather than Laravel's
notification, which would arrive with the framework's logo, button and footer
as the first thing anybody ever receives from CluPilot. It also has to leave
through the SYSTEM mailbox like every other account mail, which the
notification path does not do on its own.

The link is signed and expires, and the address is part of what is signed — so
a link issued to a mistyped address stops working the moment the address is
corrected, rather than confirming an address nobody can read. Tests cover the
tampered link, the expired one, the corrected address, and one account trying
to confirm another.

The waiting page is ours (Fortify has views off) and offers the only two things
that move somebody forward: send it again, or sign out and register with the
address they meant. The resend is throttled, because otherwise it is a button
that makes the server send mail to an arbitrary address as fast as it can be
clicked. Somebody already confirmed is redirected away from it — the `verified`
middleware only guards the other direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 23:43:20 +02:00
Claude 935c9ae6ac Let people change their own password, and a location be switched off while editing it
tests / pest (push) Successful in 7m29s Details
tests / assets (push) Successful in 19s Details
tests / release (push) Successful in 4s Details
Three gaps, all of the "half-built" kind.

Nobody could change a password. Fortify's updatePasswords feature was switched
off, so an account created with a generated password kept it until someone
opened a shell on the server — including the owner's own. Both settings pages
have the form now, sharing one concern, because two copies of a password rule is
how one of them ends up weaker. It goes through Fortify's own action rather than
hashing here, and the current password is required: an unlocked machine should
not be two keystrokes away from locking its owner out.

The datacenter edit form had no active switch. The column and the scope existed,
and the list page has a toggle — but the form you open to change the thing did
not offer the one lifecycle action a location actually needs. Switching one off
now says what it does NOT do: existing hosts keep running. Once, on the actual
transition, and as the only message — the generic "saved" toast would otherwise
replace it, and an already-inactive location would have announced its own
deactivation every time its name was edited.

The staff table's actions cell is empty for your own row, because you cannot
revoke yourself. With a one-person team the column was therefore always blank
and read as broken rather than as a rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 07:49:07 +02:00
nexxo 0b2d762b88 fix(auth): scope registration throttle to its own route (per-IP), not all Fortify endpoints
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:41:30 +02:00
nexxo 35a86c413c fix(auth): throttle Fortify endpoints (registration had no limiter — signup-spam guard)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:38:44 +02:00
nexxo 88957f8f57 feat(auth): enterprise split login + signup (Fortify registration) via frontend-design
- login/register redesigned as a split brand-panel + clean form (Apple-ish
  enterprise look), cross-linked; brand panel uses accent gradient + tokens
- Fortify registration enabled; /register full-page Livewire + RegisterTest

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:16:27 +02:00
nexxo 254a7d46a0 feat(portal): Fortify auth + Login/2FA/Dashboard + component kit
Backend (Fortify):
- laravel/fortify with TOTP two-factor + recovery codes; User uses
  TwoFactorAuthenticatable; 2FA credentials hidden from serialization.
- Views off — pages are full-page class-based Livewire components (R1/R2);
  Fortify handles POST actions. Home redirect -> /dashboard. v1 scope: login +
  2FA only (no public register/reset/passkeys). Seeder gated to local/testing.

Component kit (Blade, token-based, a11y):
- button, input, checkbox, alert, card, badge, stat-tile, otp-input (Alpine,
  auto-advance/paste, -safe submit), progress-stepper, nav-item, icon
  (Lucide), plus layouts/portal-app app-shell (sidebar drawer + topbar + menu).

Screens (localized DE/EN, R16):
- Login (form -> login.store), Two-factor challenge (OTP + recovery fallback),
  Dashboard (KPI stat tiles, instance card, provisioning stepper fixtures,
  activity). Routes English (R13).

Tests + verification:
- Pest: 14 green (login ok/invalid/throttle, dashboard guard, component render).
- R12 browser (Puppeteer, prod assets): /, /login, /two-factor-challenge and
  the authenticated /dashboard all HTTP 200 with ZERO console errors; login
  flow verified end-to-end.
- Test isolation fixed (force test env over injected .env).
- Reviewed with Codex (R15): 4 rounds, all findings fixed, final pass clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 01:20:25 +02:00