4 Commits

Author SHA1 Message Date
librelad
ca7fca5007 feat(matrix): user-management tools, and stop shipping a file named *.config
Five Tools-tab actions backed by Synapse's admin API: create account, list
users, reset password, set admin, deactivate. All driven through
`docker exec matrix-synapse python` — the image has no curl, python is what
Synapse itself runs on, and talking to localhost:8008 means the tools work the
same LAN-only or behind Traefik and never depend on the published port.

Two Matrix facts are surfaced rather than hidden: a user ID is permanent, and
there is no delete — deactivation is terminal and burns the ID. The tool is
named "Deactivate" for that reason. Both destructive actions refuse to touch the
account the tools themselves authenticate as, which would otherwise lock the
Tools tab out of the server it manages.

The admin token is cached under data/. Logging in per invocation looked tidier
and was wrong: Synapse rate-limits /login, so a few tools in succession failed
with "Too Many Requests" — the tools were throttling themselves. One login,
reused, with a single re-login on 401 and a 429 retry that honours
retry_after_ms.

Also renames the Synapse logging config from log.config to log.yaml. The config
loader runs

    find "$containers_dir" -maxdepth 3 -type f -name '*.config'

and `source`s every match as bash. The file lands at <app>/data/log.yaml, which
is exactly depth 3, so under its old name EVERY libreportal command sourced a
YAML document as a shell script. It printed "command not found" per line and
took a CLI call from 1 second to 100. Only resources/ is pruned from that scan,
not data/ — so *.config is effectively a reserved extension anywhere under an
app directory, not just at its root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:25:29 +01:00
librelad
a96a3a69a1 fix(linkding): declare the admin keys its auth adapter writes
linkding_auth.sh persists ADMIN_USER and ADMIN_PASSWORD when the first admin is
created, and keeps the password in step on later resets of that account, but
linkding.config declared neither — so both writes were no-ops and the WebUI
credentials card never had anything to show. Predates the slot work; it only
became visible once authPersistCfg started warning instead of failing silently.

Added empty rather than RANDOMIZED*, because unlike bookstack or nextcloud
nothing seeds a linkding account at install — the first user is created from the
WebUI. A generated password would name an account that does not exist, and the
card would display a password that cannot log in. Unslotted for the same reason:
the slot number marks a value the installer generates, and this one is written at
runtime by the tool.

No AUTH_PROFILE key: nothing reads it (it exists only in a comment in
auth_adapter.sh), and adding an unread key is what was just cleaned up elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:24:28 +01:00
librelad
4eb5fa6434 feat(matrix,stoat): run without a domain, on the LAN or a VPN
Both apps demanded a domain and Traefik. That was over-constrained: LibrePortal
ships WireGuard, Headscale and private ports, so LAN and VPN-only is a
first-class deployment here, and Rocket.Chat and Mattermost already prove chat
apps work fine on http://<lan-ip>:<port>.

The gate on Matrix rested on a mistake of mine: server_name being permanent.
server_name and public_baseurl are independent — the identity can be a domain
you own with no DNS behind it while clients reach the server on a LAN address,
so federation can be switched on later by adding DNS and TLS, with no rebuild
and no lost history. CFG_MATRIX_SERVER_NAME now exposes exactly that, and the
install warns when it falls back to the machine's IP.

What is genuinely lost without a domain is stated where it belongs, at install:
Matrix cannot federate and Element's mobile apps want HTTPS; Stoat cannot do
camera or microphone, because browsers gate getUserMedia on a secure context
and a VPN does not change that, the check being on the URL scheme.

Both now derive their URL from the port that was actually allocated. Since ports
are only assigned during compose-up, each writes a best guess before start and
corrects it afterwards, restarting only when the value really changed.

Three bugs found while proving it works end to end:

- The Synapse image writes /data as its UID/GID env, default 991, which under
  rootless is a host sub-UID owning nothing — so the generated signing key could
  not be moved by the install user. Both the generate container and the service
  now run as the same identity USER_TAG resolves to.

- Element's config.json is bind-mounted as a file, and docker silently creates a
  DIRECTORY when the source is missing. An early return left exactly that
  landmine, which then broke every later run. It is written first now, and a
  stale directory is cleared.

- A successful admin registration was reported as an error: checkSuccess read $?
  after an intervening [[ ]] test rather than the command's own status.

Verified with no domain and no Traefik installed: Synapse answers
/_matrix/client/versions and /health on http://<ip>:<port>, admin login returns
a token, and Element is configured against the corrected base_url.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:51:16 +01:00
librelad
b9e334dc49 matrix: add Synapse + Element as a federated chat app
Synapse on Postgres plus the Element web client, on two subdomains: the
homeserver on matrix.<domain> (which becomes server_name, so IDs read
@alice:matrix.<domain>) and Element on element.<domain>.

Two hosts rather than one because server_name then matches the host Traefik
already terminates TLS for, so 'serve_server_wellknown: true' is all the
federation delegation needed and nothing has to be published at the apex
domain — which this app has no way to configure.

CFG_MATRIX_AUTHELIA is pinned false and documented: forward-auth in front of
/_matrix locks out every client and every federating peer, since they carry
Matrix access tokens and cannot follow a redirect. Real SSO goes through the
OIDC block in resources/homeserver.yaml instead.

The install hook generates the signing key once via upstream's own 'generate'
command and refuses to regenerate it over an existing install — a new key would
be rejected by every server that had cached the old one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:31 +01:00