diff --git a/containers/stoat/scripts/stoat_auth.sh b/containers/stoat/scripts/stoat_auth.sh index 77de63e..b649522 100644 --- a/containers/stoat/scripts/stoat_auth.sh +++ b/containers/stoat/scripts/stoat_auth.sh @@ -9,19 +9,87 @@ # # What that rules out, and why: # -# Password reset — Stoat hashes with argon2 through its own authifier layer. -# Reimplementing that in bash means matching its parameters exactly, and -# getting them subtly wrong writes a hash nothing can verify, silently locking -# the account out with no error at the time. Users reset their own password -# through the app; that path is not worth counterfeiting from a button. -# # Role/permission editing — Stoat's permissions are per-server bitfields held # on server_members, not a global admin flag. There is no single "make admin" # to toggle; it is a server-by-server concept the app models properly and a -# button would misrepresent. +# button would misrepresent. Confirmed against a running instance: the user +# document holds only _id/username/discriminator, and GET /users/@me adds only +# relationship and online. There is no privileged bit to set. # -# What is left is genuinely useful and genuinely safe: see who exists, and turn -# an account off or back on. +# Password reset WAS excluded for the same reason — Stoat hashes with argon2 +# through its own authifier layer, and a hand-rolled hash that is subtly wrong +# writes an account nothing can verify, with no error at the time. That objection +# is answered not by reimplementing the hash but by refusing to: authifier's own +# password_reset field takes a token, and its reset endpoint does the hashing. +# See authAdapter_stoat_setPassword. + +# Host-local API base for this app, e.g. http://127.0.0.1:8860/api. +# +# 127.0.0.1 rather than the published LAN address: this only ever runs on the +# box itself, and the loopback route does not depend on which interface the +# advertised URL happens to name. +_stoatApiLocal() { + local app_name="${1:-stoat}" + local compose="$containers_dir$app_name/docker-compose.yml" + local ports external + ports=$(tagsManagerGetTagContent "$compose" "PORTS_TAG_1" 2>/dev/null) + external="${ports%%:*}" + [[ -z "$external" || "$external" == PORTS_DATA* ]] && return 1 + printf 'http://127.0.0.1:%s/api' "$external" +} + +# JSON-encode one value, so an apostrophe in a password cannot end the string. +_stoatJson() { printf '%s' "$1" | python3 -c 'import sys,json;print(json.dumps(sys.stdin.read()))'; } + +# True when the instance already holds at least one account. +_stoatAccountExists() { + local n + n=$(_stoatMongo 'print(db.accounts.countDocuments({}))' | tr -dc '0-9') + [[ -n "$n" && "$n" != "0" ]] +} + +# Register an account and finish onboarding, which is what actually makes it a +# usable identity: `accounts` holds the login, `users` the handle, and Stoat +# creates the second only when onboarding completes. An account left un-onboarded +# can sign in and then sits on a "pick a username" screen forever, and — worse for +# a first account — has not yet taken instance ownership. +# +# Done over HTTP rather than by writing Mongo directly because passwords go +# through Stoat's own argon2 layer. Reimplementing that in bash means matching +# its parameters exactly, and a subtly wrong hash writes an account nothing can +# verify: no error at the time, just a login that never works. +_stoatCreateAccount() { + local api="$1" email="$2" pass="$3" user="$4" + local body out token + + body="{\"email\":$(_stoatJson "$email"),\"password\":$(_stoatJson "$pass")}" + out=$(runFileOp curl -sS --max-time 20 -X POST "${api}/auth/account/create" \ + -H 'Content-Type: application/json' -d "$body" 2>&1) + # A successful create returns 204 with no body; anything printed is an error. + if [[ -n "$out" && "$out" != *'"result"'* ]]; then + isError "Stoat account create failed: $(printf '%s' "$out" | tr -d '\n' | head -c 200)" + return 1 + fi + + out=$(runFileOp curl -sS --max-time 20 -X POST "${api}/auth/session/login" \ + -H 'Content-Type: application/json' -d "$body" 2>&1) + token=$(printf '%s' "$out" | python3 -c 'import sys,json +try: print(json.load(sys.stdin).get("token","")) +except Exception: print("")' 2>/dev/null) + if [[ -z "$token" ]]; then + isError "Stoat login after create failed: $(printf '%s' "$out" | tr -d '\n' | head -c 200)" + return 1 + fi + + out=$(runFileOp curl -sS --max-time 20 -X POST "${api}/onboard/complete" \ + -H 'Content-Type: application/json' -H "X-Session-Token: ${token}" \ + -d "{\"username\":$(_stoatJson "$user")}" 2>&1) + if [[ "$out" == *'"type"'*'"error"'* || "$out" == *UsernameTaken* || "$out" == *InvalidUsername* ]]; then + isError "Stoat onboarding failed for '${user}': $(printf '%s' "$out" | tr -d '\n' | head -c 200)" + return 1 + fi + return 0 +} _stoatMongo() { runFileOp docker exec -i stoat-database mongosh revolt --quiet --eval "$1" 2>&1 @@ -124,3 +192,93 @@ authAdapter_stoat_enableUser() { _stoatSetDisabled "$who" "false" "Enabling" || return 1 isSuccessful "Stoat account '$who' re-enabled." } + +authAdapter_stoat_createUser() { + local email="$1" pass="$2" user="$3" + [[ -z "$email" ]] && { isError "An email is required."; return 1; } + [[ -z "$pass" ]] && pass=$(generateRandomPassword) + # Stoat needs a handle as well as a login. Derive one from the email's local + # part when the caller did not supply it, so the generic "create user" form + # (which only asks for email + password) still produces a usable account + # rather than one stuck on the pick-a-username screen. + if [[ -z "$user" ]]; then + user="${email%%@*}" + user="${user//[^a-zA-Z0-9_.]/}" + fi + + local api + api=$(_stoatApiLocal "${CFG_STOAT_APP_NAME:-stoat}") || { + isError "Could not work out Stoat's local API address."; return 1; } + + _stoatCreateAccount "$api" "$email" "$pass" "$user" || return 1 + + # Keep the config truthful when this IS the configured owner: an install that + # could not reach the API leaves those fields describing an account that does + # not exist, and creating it by hand afterwards should reconcile the two. + if [[ "$email" == "${CFG_STOAT_ADMIN_EMAIL:-}" ]]; then + authPersistCfg stoat ADMIN_PASSWORD "$pass" + authPersistCfg stoat ADMIN_USERNAME "$user" + fi + isSuccessful "Stoat account created — Handle: $user — Email: $email — Password: $pass" +} + +# Reset a password WITHOUT touching the hash ourselves. +# +# authifier already owns a reset flow: an account carries a password_reset token, +# and PATCH /auth/account/reset_password trades that token for a new password — +# hashing it with exactly the parameters the verifier expects, because it is the +# same code that verifies. So the only thing written directly is the token, which +# is inert on its own; Stoat does the part that has to be right. +# +# The alternative, writing an argon2 string into the account document, would mean +# reproducing $argon2i$v=19$m=4096,t=3,p=1 by hand — and a near miss there is +# silent, locking the holder out with no error at the time. +authAdapter_stoat_setPassword() { + local who="$1" pass="$2" + [[ -z "$who" ]] && { isError "A username or email is required."; return 1; } + [[ -z "$pass" ]] && pass=$(generateRandomPassword) + + local api + api=$(_stoatApiLocal "${CFG_STOAT_APP_NAME:-stoat}") || { + isError "Could not work out Stoat's local API address."; return 1; } + + # Short-lived and single-use: the reset endpoint consumes it, and the expiry + # bounds the window if the endpoint is never reached. + local token + token="lp$(tr -dc 'a-z0-9' &1) + if [[ -n "$out" ]]; then + # Clear the token so a failed attempt does not leave a live reset behind. + _stoatMongo "db.accounts.updateOne({\$or:[{email:$(_stoatJson "$who")}]}, {\$unset:{password_reset:''}})" >/dev/null 2>&1 + isError "Stoat rejected the new password: $(printf '%s' "$out" | tr -d '\n' | head -c 200)" + return 1 + fi + + if [[ "$who" == "${CFG_STOAT_ADMIN_EMAIL:-}" || "$who" == "${CFG_STOAT_ADMIN_USERNAME:-}" ]]; then + authPersistCfg stoat ADMIN_PASSWORD "$pass" + fi + isSuccessful "Stoat password set for $who — New password: $pass" + isNotice "Existing sessions stay valid; restart the app to force a re-login." +} diff --git a/containers/stoat/scripts/stoat_install_hooks.sh b/containers/stoat/scripts/stoat_install_hooks.sh index 52e1e69..badc2ff 100644 --- a/containers/stoat/scripts/stoat_install_hooks.sh +++ b/containers/stoat/scripts/stoat_install_hooks.sh @@ -67,9 +67,13 @@ _stoatBaseUrl() ports=$(tagsManagerGetTagContent "$compose" "PORTS_TAG_1") external="${ports%%:*}" if [[ -n "$external" && "$external" != PORTS_DATA* ]]; then - echo "http://${public_ip_v4:-localhost}:${external}" + echo "http://${local_ip_v4:-${public_ip_v4:-localhost}}:${external}" else - echo "http://${public_ip_v4:-localhost}" + # $local_ip_v4, not $public_ip_v4: these URLs are compiled into the web + # client and handed to browsers, and LibrePortal does not forward ports — + # so the WAN address an external resolver reports is unreachable for + # exactly the LAN/VPN clients this branch serves. + echo "http://${local_ip_v4:-${public_ip_v4:-localhost}}" fi } @@ -303,22 +307,94 @@ stoat_install_post_start() base=$(_stoatBaseUrl "$app_name") current=$(runFileOp grep -oP '^VITE_API_URL=\K.*' "$app_dir/.env.web" 2>/dev/null) current="${current%/api}" - [[ "$base" == "$current" ]] && return 0 + + # Guarded, NOT an early return. Claiming the owner account below has to happen + # on every install, and a domain-backed one guesses the URL correctly first + # time — so returning here when nothing needed settling silently skipped + # provisioning on exactly the installs that went most smoothly. + if [[ "$base" != "$current" ]]; then + ((menu_number++)) + echo "" + echo "---- $menu_number. Settling the Stoat public URL" + echo "" + + local video_enabled="" + [[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true" + _stoatWriteUrlFiles "$app_dir" "$base" "$video_enabled" "$CFG_STOAT_RABBITMQ_PASSWORD_1" + _stoatOwnConfigFiles "$app_dir" + isSuccessful "Public URL settled as $base (was ${current:-unset})" + + # The web client compiles VITE_* at container start, so it has to come + # back up before the corrected URL reaches a browser. + dockerComposeRestart "$app_name" + fi + + _stoatProvisionOwner "$app_name" +} + +# Register the configured owner account, closing the first-run land grab. +# +# Stoat is first-come-first-served — the first account registered on a fresh +# instance becomes the instance owner — and it ships invite_only=false with no +# captcha and no email verification. Until this ran, every install had a window +# between "the API answers" and "you got round to signing up" in which anyone who +# could reach the port could take ownership. +# +# Deliberately not fatal. A failure here leaves the instance exactly as it was +# before this hook existed (unclaimed, with the printed advice to go and register), +# which is worse than provisioning but no worse than the old behaviour — so it +# must not fail an otherwise good install of sixteen containers. +_stoatProvisionOwner() +{ + local app_name="$1" + local email="${CFG_STOAT_ADMIN_EMAIL:-}" + local pass="${CFG_STOAT_ADMIN_PASSWORD_1:-}" + local user="${CFG_STOAT_ADMIN_USERNAME:-admin}" + + if [[ -z "$email" || -z "$pass" ]]; then + isNotice "No Stoat owner configured (CFG_STOAT_ADMIN_EMAIL / _PASSWORD_1) — the first account to register will own this instance." + return 0 + fi ((menu_number++)) echo "" - echo "---- $menu_number. Settling the Stoat public URL" + echo "---- $menu_number. Claiming the Stoat owner account" echo "" - local video_enabled="" - [[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true" - _stoatWriteUrlFiles "$app_dir" "$base" "$video_enabled" "$CFG_STOAT_RABBITMQ_PASSWORD_1" - _stoatOwnConfigFiles "$app_dir" - isSuccessful "Public URL settled as $base (was ${current:-unset})" + local api + api=$(_stoatApiLocal "$app_name") + if [[ -z "$api" ]]; then + isNotice "Could not work out Stoat's local API address — owner account not claimed." + return 0 + fi - # The web client compiles VITE_* at container start, so it has to come back - # up before the corrected URL reaches a browser. - dockerComposeRestart "$app_name" + # Sixteen containers start in dependency order and the API restarts until + # Mongo and RabbitMQ are both healthy, so this is a wait, not a poll-once. + isNotice "Waiting for the Stoat API at ${api} ..." + local i=0 code="" + while ((i < 90)); do + code=$(runFileOp curl -sS -o /dev/null --max-time 3 -w '%{http_code}' "${api}/" 2>/dev/null) + [[ "$code" == "200" ]] && break + sleep 2 + ((i++)) + done + if [[ "$code" != "200" ]]; then + isNotice "Stoat's API did not answer within $((90 * 2))s — owner account not claimed. Register at the URL below to take ownership." + return 0 + fi + isSuccessful "Stoat API is up." + + if _stoatAccountExists "$api"; then + isSuccessful "This instance already has accounts — leaving ownership alone." + return 0 + fi + + if _stoatCreateAccount "$api" "$email" "$pass" "$user"; then + isSuccessful "Stoat owner account created (${user} / ${email})." + stoat_owner_claimed="true" + else + isNotice "Could not create the Stoat owner account — register at the URL below to take ownership yourself." + fi } stoat_install_post() @@ -330,8 +406,26 @@ stoat_install_post() echo "" isNotice "Stoat first run:" echo "" - echo " Open ${base} and create an account — the first account" - echo " registered on a fresh instance becomes the instance owner." + if [[ "${stoat_owner_claimed:-}" == "true" ]]; then + echo " Sign in at ${base}" + echo "" + echo " Handle : ${CFG_STOAT_ADMIN_USERNAME:-administrator}" + echo " Email : ${CFG_STOAT_ADMIN_EMAIL}" + echo " Password : ${CFG_STOAT_ADMIN_PASSWORD_1}" + echo "" + # Said plainly because it is the part people get wrong: creating this + # account does not close registration. Stoat ships invite_only=false with + # no captcha and no email verification, so anyone who can reach the URL + # can still make their own account. + echo " Registration is still OPEN — anyone who can reach that URL can" + echo " sign up. Keep the port off the internet, or set invite_only in" + echo " Revolt.toml, if that is not what you want." + else + echo " Open ${base} and create an account." + echo "" + echo " Registration is open and unverified, so do it before anyone" + echo " else can reach the URL." + fi echo "" if [[ "$base" == http://* ]]; then echo " This install serves plain HTTP. Text chat, channels, roles and" diff --git a/containers/stoat/stoat.config b/containers/stoat/stoat.config index 470359f..e9fa086 100644 --- a/containers/stoat/stoat.config +++ b/containers/stoat/stoat.config @@ -57,6 +57,29 @@ CFG_STOAT_MONITORING=false # 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 diff --git a/containers/stoat/tools/stoat.tools.json b/containers/stoat/tools/stoat.tools.json index d086e3a..58033e0 100644 --- a/containers/stoat/tools/stoat.tools.json +++ b/containers/stoat/tools/stoat.tools.json @@ -1,5 +1,54 @@ { "tools": [ + { + "id": "create_account", + "category": "users", + "label": "Create User Account", + "description": "Register an account and finish its onboarding, so it can sign in straight away.", + "icon": "👤", + "fields": [ + { + "name": "email", + "label": "Email", + "type": "text", + "placeholder": "user@stoat.local", + "required": true + }, + { + "name": "password", + "label": "Password", + "type": "password", + "placeholder": "Leave blank to generate" + }, + { + "name": "username", + "label": "Username (handle)", + "type": "text", + "placeholder": "Defaults to the part before @" + } + ] + }, + { + "id": "reset_password", + "category": "users", + "label": "Reset User Password", + "description": "Set a new password for an existing account. Stoat hashes it, so the account stays usable.", + "icon": "🔑", + "fields": [ + { + "name": "username", + "label": "Username or email", + "type": "text", + "required": true + }, + { + "name": "password", + "label": "New password", + "type": "password", + "placeholder": "Leave blank to generate" + } + ] + }, { "id": "list_users", "category": "users", diff --git a/containers/stoat/tools/stoat_create_account.sh b/containers/stoat/tools/stoat_create_account.sh new file mode 100644 index 0000000..be9b713 --- /dev/null +++ b/containers/stoat/tools/stoat_create_account.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +appStoatCreateAccount() { + local args="$1" + authAdapterCall stoat createUser \ + "$(authToolArg "$args" email)" \ + "$(authToolArg "$args" password)" \ + "$(authToolArg "$args" username)" +} diff --git a/containers/stoat/tools/stoat_reset_password.sh b/containers/stoat/tools/stoat_reset_password.sh new file mode 100644 index 0000000..99e6e64 --- /dev/null +++ b/containers/stoat/tools/stoat_reset_password.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +appStoatResetPassword() { + local args="$1" + authAdapterCall stoat setPassword \ + "$(authToolArg "$args" username)" \ + "$(authToolArg "$args" password)" +} diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 698312e..b9f7903 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -104,9 +104,11 @@ declare -gA LP_FN_MAP=( [appStalwartSetMode]="stalwart/tools/stalwart_set_mode.sh" [appStalwartShowDns]="stalwart/tools/stalwart_show_dns.sh" [appStatus]="app/app_status.sh" + [appStoatCreateAccount]="stoat/tools/stoat_create_account.sh" [appStoatDeleteUser]="stoat/tools/stoat_delete_user.sh" [appStoatEnableUser]="stoat/tools/stoat_enable_user.sh" [appStoatListUsers]="stoat/tools/stoat_list_users.sh" + [appStoatResetPassword]="stoat/tools/stoat_reset_password.sh" [appTraefikExtraMiddlewares_onlyoffice]="onlyoffice/scripts/onlyoffice_traefik.sh" [appTraefikResetPassword]="traefik/tools/traefik_reset_password.sh" [appTraefikSkipsDefaultMiddleware_onlyoffice]="onlyoffice/scripts/onlyoffice_traefik.sh" @@ -189,9 +191,11 @@ declare -gA LP_FN_MAP=( [authAdapter_rocketchat_listUsers]="rocketchat/scripts/rocketchat_auth.sh" [authAdapter_rocketchat_setAdmin]="rocketchat/scripts/rocketchat_auth.sh" [authAdapter_rocketchat_setPassword]="rocketchat/scripts/rocketchat_auth.sh" + [authAdapter_stoat_createUser]="stoat/scripts/stoat_auth.sh" [authAdapter_stoat_deleteUser]="stoat/scripts/stoat_auth.sh" [authAdapter_stoat_enableUser]="stoat/scripts/stoat_auth.sh" [authAdapter_stoat_listUsers]="stoat/scripts/stoat_auth.sh" + [authAdapter_stoat_setPassword]="stoat/scripts/stoat_auth.sh" [authAdapter_traefik_setPassword]="traefik/scripts/traefik_auth.sh" [authelia_install_post]="authelia/scripts/authelia_install_hooks.sh" [authelia_install_post_compose]="authelia/scripts/authelia_install_hooks.sh" @@ -969,16 +973,21 @@ declare -gA LP_FN_MAP=( [startOther]="start/start_other.sh" [startPreInstall]="start/start_preinstall.sh" [startScan]="start/start_scan.sh" + [_stoatAccountExists]="stoat/scripts/stoat_auth.sh" + [_stoatApiLocal]="stoat/scripts/stoat_auth.sh" [_stoatBaseUrl]="stoat/scripts/stoat_install_hooks.sh" + [_stoatCreateAccount]="stoat/scripts/stoat_auth.sh" [_stoatDomain]="stoat/scripts/stoat_install_hooks.sh" [stoat_install_post]="stoat/scripts/stoat_install_hooks.sh" [stoat_install_post_compose]="stoat/scripts/stoat_install_hooks.sh" [stoat_install_post_start]="stoat/scripts/stoat_install_hooks.sh" [stoat_install_pre]="stoat/scripts/stoat_install_hooks.sh" + [_stoatJson]="stoat/scripts/stoat_auth.sh" [_stoatMongo]="stoat/scripts/stoat_auth.sh" [_stoatMongoFailed]="stoat/scripts/stoat_auth.sh" [_stoatMongoWho]="stoat/scripts/stoat_auth.sh" [_stoatOwnConfigFiles]="stoat/scripts/stoat_install_hooks.sh" + [_stoatProvisionOwner]="stoat/scripts/stoat_install_hooks.sh" [_stoatSetDisabled]="stoat/scripts/stoat_auth.sh" [_stoatWriteSecrets]="stoat/scripts/stoat_install_hooks.sh" [_stoatWriteUrlFiles]="stoat/scripts/stoat_install_hooks.sh" @@ -1267,9 +1276,11 @@ declare -gA LP_FN_ROOT=( [appStalwartSetMode]="containers" [appStalwartShowDns]="containers" [appStatus]="scripts" + [appStoatCreateAccount]="containers" [appStoatDeleteUser]="containers" [appStoatEnableUser]="containers" [appStoatListUsers]="containers" + [appStoatResetPassword]="containers" [appTraefikExtraMiddlewares_onlyoffice]="containers" [appTraefikResetPassword]="containers" [appTraefikSkipsDefaultMiddleware_onlyoffice]="containers" @@ -1352,9 +1363,11 @@ declare -gA LP_FN_ROOT=( [authAdapter_rocketchat_listUsers]="containers" [authAdapter_rocketchat_setAdmin]="containers" [authAdapter_rocketchat_setPassword]="containers" + [authAdapter_stoat_createUser]="containers" [authAdapter_stoat_deleteUser]="containers" [authAdapter_stoat_enableUser]="containers" [authAdapter_stoat_listUsers]="containers" + [authAdapter_stoat_setPassword]="containers" [authAdapter_traefik_setPassword]="containers" [authelia_install_post]="containers" [authelia_install_post_compose]="containers" @@ -2132,16 +2145,21 @@ declare -gA LP_FN_ROOT=( [startOther]="scripts" [startPreInstall]="scripts" [startScan]="scripts" + [_stoatAccountExists]="containers" + [_stoatApiLocal]="containers" [_stoatBaseUrl]="containers" + [_stoatCreateAccount]="containers" [_stoatDomain]="containers" [stoat_install_post]="containers" [stoat_install_post_compose]="containers" [stoat_install_post_start]="containers" [stoat_install_pre]="containers" + [_stoatJson]="containers" [_stoatMongo]="containers" [_stoatMongoFailed]="containers" [_stoatMongoWho]="containers" [_stoatOwnConfigFiles]="containers" + [_stoatProvisionOwner]="containers" [_stoatSetDisabled]="containers" [_stoatWriteSecrets]="containers" [_stoatWriteUrlFiles]="containers" @@ -2466,9 +2484,11 @@ appSetupComposeTags_wireguard() { unset -f appSetupComposeTags_wireguard; __lpAu appStalwartSetMode() { unset -f appStalwartSetMode; __lpAutoload "${install_containers_dir}stalwart/tools/stalwart_set_mode.sh"; appStalwartSetMode "$@"; } appStalwartShowDns() { unset -f appStalwartShowDns; __lpAutoload "${install_containers_dir}stalwart/tools/stalwart_show_dns.sh"; appStalwartShowDns "$@"; } appStatus() { unset -f appStatus; __lpAutoload "${install_scripts_dir}app/app_status.sh"; appStatus "$@"; } +appStoatCreateAccount() { unset -f appStoatCreateAccount; __lpAutoload "${install_containers_dir}stoat/tools/stoat_create_account.sh"; appStoatCreateAccount "$@"; } appStoatDeleteUser() { unset -f appStoatDeleteUser; __lpAutoload "${install_containers_dir}stoat/tools/stoat_delete_user.sh"; appStoatDeleteUser "$@"; } appStoatEnableUser() { unset -f appStoatEnableUser; __lpAutoload "${install_containers_dir}stoat/tools/stoat_enable_user.sh"; appStoatEnableUser "$@"; } appStoatListUsers() { unset -f appStoatListUsers; __lpAutoload "${install_containers_dir}stoat/tools/stoat_list_users.sh"; appStoatListUsers "$@"; } +appStoatResetPassword() { unset -f appStoatResetPassword; __lpAutoload "${install_containers_dir}stoat/tools/stoat_reset_password.sh"; appStoatResetPassword "$@"; } appTraefikExtraMiddlewares_onlyoffice() { unset -f appTraefikExtraMiddlewares_onlyoffice; __lpAutoload "${install_containers_dir}onlyoffice/scripts/onlyoffice_traefik.sh"; appTraefikExtraMiddlewares_onlyoffice "$@"; } appTraefikResetPassword() { unset -f appTraefikResetPassword; __lpAutoload "${install_containers_dir}traefik/tools/traefik_reset_password.sh"; appTraefikResetPassword "$@"; } appTraefikSkipsDefaultMiddleware_onlyoffice() { unset -f appTraefikSkipsDefaultMiddleware_onlyoffice; __lpAutoload "${install_containers_dir}onlyoffice/scripts/onlyoffice_traefik.sh"; appTraefikSkipsDefaultMiddleware_onlyoffice "$@"; } @@ -2551,9 +2571,11 @@ authAdapter_rocketchat_enableUser() { unset -f authAdapter_rocketchat_enableUser authAdapter_rocketchat_listUsers() { unset -f authAdapter_rocketchat_listUsers; __lpAutoload "${install_containers_dir}rocketchat/scripts/rocketchat_auth.sh"; authAdapter_rocketchat_listUsers "$@"; } authAdapter_rocketchat_setAdmin() { unset -f authAdapter_rocketchat_setAdmin; __lpAutoload "${install_containers_dir}rocketchat/scripts/rocketchat_auth.sh"; authAdapter_rocketchat_setAdmin "$@"; } authAdapter_rocketchat_setPassword() { unset -f authAdapter_rocketchat_setPassword; __lpAutoload "${install_containers_dir}rocketchat/scripts/rocketchat_auth.sh"; authAdapter_rocketchat_setPassword "$@"; } +authAdapter_stoat_createUser() { unset -f authAdapter_stoat_createUser; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; authAdapter_stoat_createUser "$@"; } authAdapter_stoat_deleteUser() { unset -f authAdapter_stoat_deleteUser; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; authAdapter_stoat_deleteUser "$@"; } authAdapter_stoat_enableUser() { unset -f authAdapter_stoat_enableUser; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; authAdapter_stoat_enableUser "$@"; } authAdapter_stoat_listUsers() { unset -f authAdapter_stoat_listUsers; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; authAdapter_stoat_listUsers "$@"; } +authAdapter_stoat_setPassword() { unset -f authAdapter_stoat_setPassword; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; authAdapter_stoat_setPassword "$@"; } authAdapter_traefik_setPassword() { unset -f authAdapter_traefik_setPassword; __lpAutoload "${install_containers_dir}traefik/scripts/traefik_auth.sh"; authAdapter_traefik_setPassword "$@"; } authelia_install_post() { unset -f authelia_install_post; __lpAutoload "${install_containers_dir}authelia/scripts/authelia_install_hooks.sh"; authelia_install_post "$@"; } authelia_install_post_compose() { unset -f authelia_install_post_compose; __lpAutoload "${install_containers_dir}authelia/scripts/authelia_install_hooks.sh"; authelia_install_post_compose "$@"; } @@ -3331,16 +3353,21 @@ startLoad() { unset -f startLoad; __lpAutoload "${install_scripts_dir}start/star startOther() { unset -f startOther; __lpAutoload "${install_scripts_dir}start/start_other.sh"; startOther "$@"; } startPreInstall() { unset -f startPreInstall; __lpAutoload "${install_scripts_dir}start/start_preinstall.sh"; startPreInstall "$@"; } startScan() { unset -f startScan; __lpAutoload "${install_scripts_dir}start/start_scan.sh"; startScan "$@"; } +_stoatAccountExists() { unset -f _stoatAccountExists; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatAccountExists "$@"; } +_stoatApiLocal() { unset -f _stoatApiLocal; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatApiLocal "$@"; } _stoatBaseUrl() { unset -f _stoatBaseUrl; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatBaseUrl "$@"; } +_stoatCreateAccount() { unset -f _stoatCreateAccount; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatCreateAccount "$@"; } _stoatDomain() { unset -f _stoatDomain; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatDomain "$@"; } stoat_install_post() { unset -f stoat_install_post; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_post "$@"; } stoat_install_post_compose() { unset -f stoat_install_post_compose; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_post_compose "$@"; } stoat_install_post_start() { unset -f stoat_install_post_start; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_post_start "$@"; } stoat_install_pre() { unset -f stoat_install_pre; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_pre "$@"; } +_stoatJson() { unset -f _stoatJson; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatJson "$@"; } _stoatMongo() { unset -f _stoatMongo; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatMongo "$@"; } _stoatMongoFailed() { unset -f _stoatMongoFailed; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatMongoFailed "$@"; } _stoatMongoWho() { unset -f _stoatMongoWho; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatMongoWho "$@"; } _stoatOwnConfigFiles() { unset -f _stoatOwnConfigFiles; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatOwnConfigFiles "$@"; } +_stoatProvisionOwner() { unset -f _stoatProvisionOwner; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatProvisionOwner "$@"; } _stoatSetDisabled() { unset -f _stoatSetDisabled; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_auth.sh"; _stoatSetDisabled "$@"; } _stoatWriteSecrets() { unset -f _stoatWriteSecrets; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatWriteSecrets "$@"; } _stoatWriteUrlFiles() { unset -f _stoatWriteUrlFiles; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatWriteUrlFiles "$@"; }