Commit Graph

8 Commits (e376b571be3c9dd953596d5b878c17f8f3239abb)

Author SHA1 Message Date
nexxo e376b571be Read the support mailbox into the console
tests / pest (push) Failing after 7m59s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details
No, not an API token: mailcow's API administers domains and mailboxes —
it does not read mail. Reading a mailbox is IMAP whoever runs the server,
so that is what this is. Host, user and folder are console settings; the
password is a vault entry, like every other credential that opens
something.

IMAP by hand rather than a dependency. Four verbs are needed — LOGIN,
SELECT, SEARCH UNSEEN, FETCH — and the parsing that follows is the real
work either way. It is bounded by what the console shows: a sender, a
subject, the words a person wrote, and the NAMES of anything attached.
Real mail from real people arrives encoded, so it handles RFC 2047
subjects (every German subject with an umlaut in it), quoted-printable
and base64 bodies, Windows charsets, and multipart — text wins over HTML,
because a console is not a mail client and the words are in the text
part. A message it cannot make sense of is skipped and logged, never
guessed at: a garbled question in front of an operator is worse than one
they go and read in the mailbox, where it still is.

ATTACHMENTS ARE NOT STORED. Only their names and sizes, so an operator
knows something was attached and can go and look if it matters. Keeping
whatever a stranger chooses to send would make this application a malware
store with a web interface in front of it.

Whose a mail is, is decided by the sender's ADDRESS and by nothing else —
not by a name in the subject, not by anything in the body. A mail is easy
to write and this decision attaches a stranger's words to a real
customer's file; there is a test that forges the name and the subject and
still gets nothing. An address nobody recognises stays unassigned, shown
FIRST and never hidden: that is a new enquiry, or a customer writing from
their private account, and it is the mail that must not be missed. An
operator can place it by hand from the same row.

Nothing is deleted on the mail server, ever. Messages are flagged seen
once the row is safely written — and only then, because flagging first
and failing after would lose the message with no second copy anywhere.
Filing one away is this console's own state; the mailbox is untouched.

Two fixes on the way past: the customer page printed
"customers.status.active" and "support.status.open" — a lang key with no
file behind it renders as itself and Laravel says nothing — and the same
page is now covered by a test proving each list is scoped to the customer
in the URL, which was reported as "unterhalb steht immer das selbe".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:41:29 +02:00
nexxo 90131f088a Add a raw .env editor to the Integrations page, with a net under it
Everything the console has no field for is still real — MAIL_*, DB_*,
APP_KEY, whichever key nobody has built a form for yet — and without a way to
touch it from here, the operator needs a shell and the console page misses
its own point. EnvFileEditor is the net, not just the warning:

1. Validates before writing. A line that is neither blank, nor a comment, nor
   KEY=value is rejected outright, and an empty file (syntactically "valid" by
   that rule, but not survivable) is refused too. write() never touches the
   file before checking the new content.
2. Backs up before every write that actually happens — a timestamped copy
   beside .env, before the new content lands. Never pruned automatically; the
   page says so, next to where it says where they land.
3. Names the keys a mistake here can lock an operator out with — APP_KEY,
   DB_*, REDIS_*, SESSION_* — rather than a blanket warning nobody reads.
4. Gated by secrets.manage and the same confirmed password the vault entries
   use — this is the one place on the page that can reach every credential
   the vault otherwise keeps write-only.
5. Says plainly what saving does not do: queue, queue-provisioning, scheduler
   and reverb only read .env at their own startup, and names the restart
   command plus config:clear.
6. Marks which .env keys SecretVault currently overrides, so editing a line
   that a stored vault value already shadows does not look broken.

Two issues surfaced by Codex review and fixed before this commit: the raw
file content stayed in the Livewire component snapshot after the password
confirmation window expired on its own (not only on an explicit re-lock), and
the first save on an installation with no .env yet failed trying to back up a
file that was never there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:51:27 +02:00
nexxo 864126ec7f Add the SSH identity to the vault, and give deployment config a console page
tests / pest (push) Failing after 7m28s Details
tests / assets (push) Successful in 19s Details
tests / release (push) Has been skipped Details
The SSH private key CluPilot deploys to every host is now stored through
SecretVault (ssh.private_key), preferred over CLUPILOT_SSH_PRIVATE_KEY(_PATH)
at all three real consumers — SshTraefikWriter and HostStep::keyLogin.
kuma.password/kuma.totp stay in .env: they authenticate a separate Python
container at boot, and nothing in this app reads them, so the vault would
have nothing to wire them to.

A new /admin/infrastructure page, backed by App\Support\ProvisioningSettings,
makes the non-secret deployment settings that used to force a shell —
DNS zone, WireGuard hub endpoint and public key, Traefik's dynamic-config
path, the SSH public key, and the monitoring bridge URL — visible and
editable from the console, each wired to every real consumer. WG subnet/hub
IP and Kuma's own connection details stay in .env: the compose file and a
separate container read those before this application ever does.
2026-07-29 00:52:44 +02:00
nexxo 87c49de6e5 Move the console's identity out of the customer table 2026-07-28 10:31:43 +02:00
nexxo 3e4d5472a7 Move the one SMTP account into the mailbox table it outgrew
The seed migration also guards against SECRETS_KEY being unset at migrate
time (encrypting the .env password would otherwise throw and take the
whole migration down, on a fresh install as much as in the test suite),
and the four tests/Feature/Mail files written before this migration reset
the mailbox table in their beforeEach so they keep testing what they did
before mailboxes.key collided with the seeded rows.
2026-07-28 00:24:54 +02:00
nexxo 93d409e18f Pull the secrets encrypter out where two callers can share it 2026-07-27 21:09:10 +02:00
nexxo 3699c7cdb9 Ask the tunnel gateway where it actually listens
tests / pest (push) Successful in 7m15s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Successful in 5s Details
The console has been unreachable over the VPN, and the cause was the readiness
probe rather than the tunnel. The gateway binds to the WireGuard hub address
alone; the probe asked 127.0.0.1, where nothing has ever listened. It could only
fail, so VPN_READY stayed false, and on that basis the application withheld the
`DNS = 10.66.0.1` line from every client config it issued. A phone then built
the tunnel, asked its normal resolver for the console hostname, got the public
address and had its connection closed — indistinguishable from "this site does
not exist", which is exactly what it looked like.

Probing over TLS on the bare address would not have worked either: the site
matches on the console's hostname, so a request without SNI is offered no
certificate. The gateway now answers a plain-HTTP health port on the hub
address, which removes TLS, SNI and name resolution from the question and
answers only what is being asked — is this gateway listening, in this network
namespace, right now. Caddy refuses to start when the certificate is unreadable,
so a health port that answers still proves the whole file loaded.

Also here, all found while looking:

- Icons pushed their label onto a second line and rendered a size larger than
  asked for. Tailwind's preflight makes an svg display:block, and `.size-4` and
  `.size-5` have equal specificity, so stylesheet order decided — and it emits
  size-4 first. Every icon written as 16px was silently 20px. Recorded as R18.
- Four error pages printed `errors.404.hint` in place of a sentence: the lang
  files give those codes a null hint and Laravel returns the key for a null line.
- The Developer role had no label in either language, so the dropdown showed
  `admin_settings.role_developer`.
- The secrets area held one key, which is not worth a password gate. It now
  carries the credentials that actually stop the business when they expire —
  DNS, monitoring, SMTP — and the test button appears only where a checker
  exists, instead of reporting on Stripe whatever was being looked at.
- The update button said nothing about when a queued run would start or where a
  running one had got to; both are now shown, and a failure names its step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 14:43:23 +02:00
Claude a58faf3f85 Manage the Stripe key from the console, behind a password and a test
tests / pest (push) Successful in 8m56s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Successful in 4s Details
Changing a key meant a shell, a file edit and a cache rebuild — and the person
who owns the Stripe account is not necessarily the person who owns the server.

Two gates, not one. The capability decides who may open the page; every operator
has console.view, and that must not mean "can read the payment key". The
password decides whether this SESSION may see or change anything, because the
realistic threat is not a stranger but an unlocked machine, and a session is
exactly what that hands over. Both are re-checked server-side on every action —
a Livewire action is reachable by anyone who can post to /livewire/update.

The value is stored encrypted under a key of its own, SECRETS_KEY, and the vault
refuses to work without it rather than falling back to APP_KEY: rotating APP_KEY
is ordinary maintenance and would otherwise destroy every stored credential,
discovered when Stripe stops answering. It is read where it is used, not
overlaid onto config at boot — an overlay adds a query to every request
including the public site, and leaves queue workers holding whatever was true
when they started. It is never shown again, only outlined, and never enters a
Livewire property that would carry it to the browser and back in the snapshot.

A registry, not an env editor: a form that can set any environment variable is a
privilege-escalation primitive, and one bad value bricks the installation with
no way back through that same form.

The test button is the part that matters. It reports which Stripe account the
key belongs to, whether it is LIVE or test — the most expensive mistake here is
pasting one where the other belongs, and both look identical in a form — and
which webhook endpoints exist with the events each subscribes to. A key can be
perfectly valid while the endpoint listens for the wrong five events, and
nothing fails until a payment goes unrecorded.

The webhook signing secret deliberately stays in .env. It is read on every
incoming payment event; in the database, a database problem becomes silently
failing signature checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:01:16 +02:00