feat: add PUT /api/0/users/me for user self-service profile updates

Allows any authenticated user to update their own full_name, avatar,
notification_channels, and password via the config YAML write path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 11:45:09 -04:00
parent 9a0baf3c78
commit 60c692cefc
2 changed files with 142 additions and 0 deletions
+57
View File
@@ -1173,6 +1173,62 @@ async def start(
return web.json_response({"ok": True})
async def api_user_self_put(request):
"""PUT /api/0/users/me — update own full_name, avatar, notification_channels, password."""
user, err = _require_auth(request)
if err:
return err
if user is None:
return web.json_response({"error": "Authentication required"}, status=401)
if not _config_path:
return web.json_response({"error": "Config path not available"}, status=503)
try:
body = await request.json()
except Exception:
return web.json_response({"error": "Invalid JSON"}, status=400)
if not isinstance(body, dict):
return web.json_response({"error": "Invalid JSON"}, status=400)
username = user.username
password_change = body.get("password")
if password_change:
current_pw = password_change.get("current", "")
new_pw = password_change.get("new", "")
if not new_pw:
return web.json_response({"error": "New password cannot be empty"}, status=400)
if not users_mod.authenticate(username, current_pw):
return web.json_response({"error": "Current password incorrect"}, status=403)
try:
data = configio_mod.read_roundtrip(_config_path)
if "users" not in data or data["users"] is None:
data["users"] = {}
user_entry = dict(data["users"].get(username) or {})
if "full_name" in body:
user_entry["full_name"] = str(body["full_name"])
if "avatar" in body:
user_entry["avatar"] = str(body["avatar"])
if "notification_channels" in body:
user_entry["notification_channels"] = list(body["notification_channels"])
if password_change:
user_entry["password"] = users_mod.hash_password(password_change["new"])
data["users"][username] = user_entry
configio_mod.write_config(_config_path, data)
except Exception as exc:
logger.error("User self-update failed: %s", exc)
return web.json_response({"error": str(exc)}, status=500)
if hasattr(config, "reload"):
await config.reload()
users_mod.load_users(config)
return web.json_response({"ok": True})
app = web.Application()
app.add_routes(
[
@@ -1189,6 +1245,7 @@ async def start(
# Users
web.get("/api/0/users", api_users),
web.get("/api/0/users/me", api_user_self),
web.put("/api/0/users/me", api_user_self_put),
web.get("/api/0/users/{username}/avatar", api_user_avatar),
# Config API (admin)
web.get("/api/0/config", api_config_get),