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>
This commit is contained in:
parent
701f014619
commit
93ec260298
@ -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' </dev/urandom | head -c 40)"
|
||||
|
||||
local out
|
||||
out=$(_stoatMongo "
|
||||
const who = $(_stoatJson "$who");
|
||||
let acct = null;
|
||||
if (who.includes('@')) {
|
||||
acct = db.accounts.findOne({email: who});
|
||||
} else {
|
||||
const u = db.users.findOne({username: who});
|
||||
if (u) acct = db.accounts.findOne({_id: u._id});
|
||||
}
|
||||
if (!acct) { print('LP_MISSING'); } else {
|
||||
db.accounts.updateOne({_id: acct._id}, {\$set: {password_reset: {token: $(_stoatJson "$token"), expiry: new Date(Date.now() + 600000).toISOString()}}});
|
||||
print('LP_OK');
|
||||
}
|
||||
")
|
||||
_stoatMongoFailed "$out" "Password reset for $who" && return 1
|
||||
[[ "$out" == *LP_MISSING* ]] && { isError "No Stoat account matching '$who'."; return 1; }
|
||||
[[ "$out" != *LP_OK* ]] && { isError "Password reset for $who failed: $out"; return 1; }
|
||||
|
||||
out=$(runFileOp curl -sS --max-time 20 -X PATCH "${api}/auth/account/reset_password" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"token\":$(_stoatJson "$token"),\"password\":$(_stoatJson "$pass")}" 2>&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."
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
9
containers/stoat/tools/stoat_create_account.sh
Normal file
9
containers/stoat/tools/stoat_create_account.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
appStoatCreateAccount() {
|
||||
local args="$1"
|
||||
authAdapterCall stoat createUser \
|
||||
"$(authToolArg "$args" email)" \
|
||||
"$(authToolArg "$args" password)" \
|
||||
"$(authToolArg "$args" username)"
|
||||
}
|
||||
8
containers/stoat/tools/stoat_reset_password.sh
Normal file
8
containers/stoat/tools/stoat_reset_password.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
appStoatResetPassword() {
|
||||
local args="$1"
|
||||
authAdapterCall stoat setPassword \
|
||||
"$(authToolArg "$args" username)" \
|
||||
"$(authToolArg "$args" password)"
|
||||
}
|
||||
@ -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 "$@"; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user