librelad 93ec260298 Provision a Stoat owner account, add create/reset user tools
Stoat shipped with no account and no way to make one from LibrePortal. It is
first-come-first-served, with invite_only=false, no captcha and no email
verification, so every install left a window between the API answering and
someone signing up in which anyone who could reach the port could take the
instance. The installer now claims the configured account as soon as the API
responds, and prints the credentials instead of "go and register".

Provisioning goes over HTTP, not Mongo: an account needs a login AND a
completed onboarding (accounts holds one, users the other) and passwords go
through Stoat's argon2 layer. Failure is deliberately non-fatal — it leaves the
instance exactly as it was before this existed, which must not fail an
otherwise good install of sixteen containers.

Both obvious config defaults are rejected by Stoat, which is only visible as a
failed install, so both are chosen against its rules: example.com comes back
DisallowedContactSupport (reserved domain) hence admin@stoat.local, and "admin"
comes back InvalidUsername (reserved) hence "administrator".

Two of the three missing adapter operations are now implemented:

- createUser: create, log in, complete onboarding. Without the last step an
  account can sign in and then sits on a pick-a-username screen forever.

- setPassword: previously excluded because hand-rolling argon2 risks writing a
  hash nothing can verify, locking the holder out with no error at the time.
  That objection is answered by refusing to hash at all — authifier already
  owns a reset flow, so this writes only its password_reset token to Mongo and
  lets PATCH /auth/account/reset_password do the hashing with the same code
  that verifies. Verified: reset by username and by email, new password logs
  in, token consumed.

setAdmin is still NOT implemented, and the header now says so with evidence
rather than assertion. Stoat has no instance-level admin flag: the user
document holds only _id/username/discriminator and GET /users/@me adds only
relationship and online. Permissions are per-server bitfields on server_members.
A "make admin" button would invent a concept the app does not have.

Also fixed two things found while testing:

- post_start returned early when the public URL needed no settling, which
  skipped everything after it — so provisioning would have been silently
  missed on exactly the domain-backed installs that guessed the URL right.

- _stoatBaseUrl advertised $public_ip_v4, the WAN address from an external
  resolver, in URLs compiled into the web client. Same fix as the APP_URL
  processor: prefer $local_ip_v4, since LibrePortal never forwards ports.

Verified end to end on a clean install: the owner account is created and
onboarded, the generated password logs in, both new tools run through
`libreportal app tool`, and a created account survives a password reset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:37:32 +01:00

137 lines
7.9 KiB
Plaintext

#
# =============================================================================
# GENERAL CONFIGURATION
# =============================================================================
# APP_NAME = name of application for use in scripts
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh)
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
# BACKUP = if true, include this application in backup operations
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
# ENABLE_VIDEO = if true, allow camera and screen sharing (needs an HTTPS origin to work in a browser)
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
#
CFG_STOAT_APP_NAME=stoat
# MULTI_INSTANCE = if true, this app can run as multiple isolated instances
# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on
# apps whose compose identity (container_name, Traefik routers, backup labels)
# is instance-safe — see scripts/instance/instance_create.sh.
# Not instance-safe, for ONE remaining reason. The service names are now all
# stoat-prefixed, so compose identity is no longer the blocker. What is: LiveKit
# advertises its own tcp_port (7881) and UDP range (50000-50100) to clients from
# livekit.yml, so those cannot be randomised per instance without generating that
# file per instance and allocating a UDP range for each. Until then a second copy
# could not bind 7881.
CFG_STOAT_MULTI_INSTANCE=false
# No prerequisites. Stoat bakes its public URL into the client bundle, but that
# URL can just as well be http://<lan-ip>:<port> — text chat, channels, roles
# and uploads all work that way over a LAN or WireGuard tunnel. Only camera and
# microphone need an HTTPS origin, because browsers refuse getUserMedia outside
# a secure context; a VPN does not change that, since the check is on the URL
# scheme. Any HTTPS satisfies it, including a self-signed Traefik certificate.
CFG_STOAT_REQUIRES=""
CFG_STOAT_BACKUP=true
CFG_STOAT_BACKUP_STRATEGY=auto
# Auto. The components of this stack are released together and do expect
# matching versions — which is exactly why they are all pinned to the same tag
# (api, events, file-server, proxy, gifbox, crond, pushd, voice-ingress all on
# one STOAT_*_VERSION_TAG). An automatic update pulls rebuilds of those pinned
# tags, so the services move together or not at all; they cannot drift apart on
# their own. Moving the stack to a new release line is the stepped Upgrade and
# remains a deliberate act.
CFG_STOAT_UPDATE_TYPE=auto
CFG_STOAT_COMPOSE_FILE=default
CFG_STOAT_HEALTHCHECK=true
# Stoat's own accounts back its clients, and /api must stay reachable without a
# forward-auth redirect in the way.
CFG_STOAT_AUTHELIA=false
CFG_STOAT_HEADSCALE=false
CFG_STOAT_ENABLE_VIDEO=true
CFG_STOAT_MONITORING=false
# Service credentials fed to the compose via #LIBREPORTAL|STOAT_<KEY>_TAG|.
# Generated on first install and preserved across reinstalls: RabbitMQ writes
# its user into ./data/rabbit on first boot and ignores the env afterwards, so a
# regenerated password would leave the broker unreachable. MinIO's root password
# is shared by the object store and the bucket-creation job, which have to agree.
CFG_STOAT_RABBITMQ_PASSWORD_1=RANDOMIZEDPASSWORD1
CFG_STOAT_MINIO_PASSWORD_1=RANDOMIZEDPASSWORD2
# Owner account, registered by the installer as soon as the API answers.
#
# Stoat is first-come-first-served: whoever registers first on a fresh instance
# becomes the instance owner, and it ships with invite_only=false, no captcha and
# no email verification. Left to the printed "go and sign up" advice, the window
# between the app answering and you getting round to it is a window in which
# anyone who can reach the port owns your instance. Claiming it during install
# closes that window.
#
# ADMIN_USERNAME is Stoat's handle (letters, digits, _ and .), separate from the
# login email. Blank ADMIN_EMAIL skips provisioning and leaves the instance with
# no account at all.
#
# Both defaults are chosen because Stoat REJECTS the obvious ones, which is only
# visible as a failed install otherwise:
# - example.com is a reserved domain and comes back DisallowedContactSupport,
# so the email uses .local. Nothing is sent to it — this build runs with
# email verification off, so the address is only ever a login identifier.
# - "admin" is a reserved username and comes back InvalidUsername, hence
# "administrator".
CFG_STOAT_ADMIN_EMAIL=admin@stoat.local
CFG_STOAT_ADMIN_USERNAME=administrator
CFG_STOAT_ADMIN_PASSWORD_1=RANDOMIZEDPASSWORD3
#
# =============================================================================
# METADATA
# =============================================================================
# CATEGORY = application category for grouping
# TITLE = display name for the application
# DESCRIPTION = short description of the application
# LONG_DESCRIPTION = detailed description of the application
# URL = source repository or documentation URL
# ACTIONS = available actions for this application
# REQUIRES_SERVICE = name of another LibrePortal app that must be installed before this one can be configured
#
CFG_STOAT_CATEGORY="communication"
CFG_STOAT_TITLE="Stoat"
CFG_STOAT_DESCRIPTION="Discord-style Chat"
CFG_STOAT_LONG_DESCRIPTION="The closest open-source match to Discord — servers, channels, roles, and LiveKit voice and video. Heavy (16 containers) and does not federate"
CFG_STOAT_URL="https://github.com/stoatchat/self-hosted"
CFG_STOAT_ACTIONS="configure|install|restart|shutdown|uninstall"
#
# =============================================================================
# NETWORK CONFIGURATION
# =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
#
CFG_STOAT_DOMAIN=1
CFG_STOAT_WHITELIST=false
CFG_STOAT_NETWORK=default
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
# - app: application name
# - name: service identifier (webui, dns, ssh, etc.)
# - external:internal: port mapping (external can be 'random' for auto-allocation)
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
# - protocol: 'tcp' or 'udp'
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
# - traefik: if true, Traefik handles this port (reverse proxy)
# - webui: if true, this port serves the main web interface
# - description: human-readable description of the service
#
# Only one HTTP port: Caddy fronts the entire stack internally, so /api, /ws,
# /autumn and the rest all arrive on this single host.
CFG_STOAT_PORT_1="stoat-caddy|webui|random:80|public|tcp|false|true|true|Web Interface||stoat"
# LiveKit's TCP fallback. Pinned rather than random on purpose: LiveKit
# advertises this exact port number to clients from livekit.yml, so a randomised
# external port would be advertised wrongly and voice would fail to connect.
# Not Traefik-managed — WebRTC is not HTTP.
CFG_STOAT_PORT_2="stoat-livekit|voice-tcp|7881:7881|public|tcp|false|false|false|LiveKit voice/video (TCP fallback)|"
# Stoat exposes no safe way to set a password or grant a role from outside the
# app, so these tools list and enable/disable only — see scripts/stoat_auth.sh.