Files
Panama/docs/superpowers/specs/2026-08-24-users-accounts-redesign.md
T

12 KiB

Users & Accounts redesign — identity, honestly

Approved mock: home-mocks/users.html (scratchpad, :8642). Spec wins over mock on conflict.

Goals

  1. Users: profile hero (stock avatar gallery + file + remove), password change with strength meter, live-validating add-user form, keep-files deletion choice, per-user management (type / reset password / unlock), auto-login with the tradeoff stated.
  2. Fingerprint: native guided enrollment (fprintd D-Bus), enrolled-finger list + removal, and the stuck state made reachable — the card renders when the authselect feature is on even with no reader, offering Turn off.
  3. Online Accounts: "This desktop" cards (Home Assistant, phone) leading; GOA account cards with confirmed removal and busy states; native add for password providers (Nextcloud, IMAP); OAuth remains the single honest handoff. Page-level availability stops being gated on transient write errors.

Non-goals: camera avatar capture, OAuth without GOA's dialog (impossible — no GIR), parental controls, group management beyond account type, username changes.

Helper extensions (pinned — as built)

scripts/panama-users (Python, unchanged language). Verbs: snapshot · stock-avatars · set-real-name USER NAME · set-icon USER PATH [X Y SIZE] · set-account-type USER standard|administrator · set-automatic-login USER true|false · set-locked USER true|false · set-password USER (stdin) · reset-password USER · create-user USERNAME REALNAME TYPE · delete-user USERNAME keep|remove.

  • set-icon USER "" clears the avatar (SetIconFile("")) — no separate verb.
  • stock-avatars prints its own shape: {"avatars":[{"name","path"}],"error":""}. Absolute paths, sorted by name; a missing /usr/share/pixmaps/faces yields []. Fedora ships 15.
  • delete-user takes keep|remove; the older keep-files|remove-files spelling still works.
  • reset-password USER → SetPasswordMode(1). No password material anywhere in the path.
  • set-account-type now carries the last-admin refusal in the helper (same sentence as delete-user), so a page that forgets the guard cannot demote the only administrator.
  • Every mutating verb still answers with the fresh snapshot plus error, exit 0.

scripts/panama-fingerprint — converted bash → Python (a D-Bus signal loop is not a thing bash can do). status and set-unlock keep their exact CLI and JSON, and still shell out to fprintd-list / authselect / panama-sudo --reason, so the existing PATH-stub seam is untouched. Verbs: status · set-unlock on|off · enroll FINGER · remove FINGER · remove-all.

  • status → {reader, readerName, enrolled, pamEnabled, unlockFeatureEnabled, error}. pamEnabled and unlockFeatureEnabled are the same fact under the old and new name. Behaviour change for C: authselect is now parsed unconditionally, so a readerless machine reports pamEnabled: true when the feature is on. That used to be forced to false, which is what made the stuck state unreachable. (This machine is in it.)
  • enroll FINGER streams one JSON object per line: {ok, stage, done, total, result, error} with stage ∈ claiming|scanning|done|failed. fprintd Claim → EnrollStart → one line per EnrollStatus → EnrollStop → Release. Cancellation is SIGTERM (stopping the Process); the helper still releases the device. Exit 0 on enroll-completed, 1 otherwise. 90s idle watchdog.
  • remove FINGER / remove-all → DeleteEnrolledFinger / DeleteEnrolledFingers2 under a Claim, and answer with the status shape so the service has one absorb path.
  • Finger vocabulary lives in services/Fingerprint.qml (allFingers, fingerLabel); the helper only shape-checks the name.
  • Fixture seam: PANAMA_FINGERPRINT_FIXTURE = JSON file {"enrollStages":N,"results":["enroll-stage-passed",…],"error":""}; every D-Bus call it stands in for is appended to PANAMA_FINGERPRINT_LOG (default <fixture>.log) as {"method","arguments"}. No bus, no gi, no reader.

scripts/panama-accounts: list and snapshot (alias; the service asks for snapshot) · set · remove · add-nextcloud SERVER USERNAME · add-imap EMAIL IMAP_HOST SMTP_HOST USERNAME. Passwords on stdin, never argv. Both adds answer with {"accounts":[…],"error":""}.

  • GOA's add flow, verified: Goa.Manager.AddAccount(provider, identity, presentation_identity, credentials a{sv}, details a{ss}) → o is fully daemon-side — goa-daemon writes accounts.conf and calls goa_utils_store_credentials_for_id_sync itself. The missing libgoa-backend GIR blocks only the client-side sign-in dialog, so password providers are genuinely addable and OAuth genuinely is not. IsSupportedProvider is checked first (owncloud, imap_smtp both true here; nextcloud is not a provider type — owncloud is).
  • Keys are GOA 3.58's own, taken from goaowncloudprovider.c / goaimapsmtpprovider.c and confirmed against this machine's accounts.conf. Nextcloud: credentials {password}, details Uri (…/remote.php/webdav/), FilesEnabled, CalendarEnabled, CalDavUri (…/remote.php/dav/), ContactsEnabled, CardDavUri, AcceptSslErrors. IMAP: credentials {imap-password, smtp-password}, details Enabled, EmailAddress, Name, Imap{Host,UserName,UseSsl,UseTls,AcceptSslErrors}, Smtp{Host,UseAuth,UserName,AuthLogin,AuthPlain,UseSsl,UseTls,AcceptSslErrors} — IMAPS + submission/STARTTLS defaults, one password for both servers.
  • The daemon validates nothing, so each add calls EnsureCredentials on the new object and removes the account again if the credentials are refused, reporting why.
  • Fixture seam: PANAMA_ACCOUNTS_FIXTURE = a JSON file in list's shape. With it set nothing imports gi; validation still runs; each verb appends {"verb","arguments","passwordBytes"} to PANAMA_ACCOUNTS_LOG (default <fixture>.log) — the password's length, never itself.

Services (A — as built)

  • UserAccounts: removeIcon() (no argument — the hero card's own avatar), stockAvatars + loadStockAvatars() (cached; call it when the gallery opens), deleteUser(user, keepFiles) — the bool inverted from the old removeFiles — resetPassword(user), setAccountTypeFor(user, type) (renamed from setAccountType), setLocked(user, locked). locked and loginTime stay in the payload for the UI.
  • Fingerprint: readerPresent, readerName, unlockFeatureEnabled, fingers (renamed from enrolled), allFingers, availableFingers, fingerLabel(f), cardVisible (= readerPresent || unlockFeatureEnabled), busy, lastError, enrolling, enrollPhase, enrollFinger, enrollStage, enrollTotal, enrollResult, refresh(), setUnlockEnabled(on), startEnroll(finger), cancelEnroll(), removeFinger(f), removeAll().
  • OnlineAccounts: available (GOA answered the last snapshot — persisted, never touched by a write), snapshotError / writeError and the derived lastError, busy (derived from the Processes), attentionCount, refresh() (cheap; call on every page open), setService, remove, addNextcloud(server, user, password), addImap(email, imapHost, smtpHost, user, password) — password via Process stdin.

UI (B)

UsersPage.qml rebuilt per mock: hero card (avatar, Change picture… opens a popover with stock gallery + "Choose a file…" → existing AvatarPicker/Cropper flow, Remove button); Account card (Full name — live-committing field, Username honest row, Account type segment with the last-admin reason as detail, Password flow with a 4-segment strength meter — local heuristic, length + classes, no network; Automatic login with the mock's tradeoff copy); Fingerprint card per mock (stuck row with NO READER badge + Turn off when unlockFeatureEnabled && !readerPresent; full experience otherwise: toggle, enrolled list with per-finger Remove…, Add a fingerprint → inline enrollment panel driven by the enroll stream — touch icon, stage counter "N of M touches", cancel; no continuous animation); Other accounts card (expandable per-user rows: type segment, Reset password ("They set a new one at next sign-in"), Delete with a keep-files dropdown [Keep the files / Remove everything]

  • two-stage confirm; locked users show Unlock; add-user form with LIVE validation — use live TextFields, not blur-committing TextFieldRow, with the username rules + 31-char cap in the detail and mirrored in the enabled predicate).

OnlineAccountsPage.qml rebuilt: gains objectName: "accounts"; "This desktop" card — Home Assistant row (state from the HomeAssistant service: connected/rooms, else "not set up", button → openSettings("my-home")) and Phone row (KdeConnect state, → openSettings("phone")); Accounts card — one expandable row per GOA account (per-service toggles disabled while busy, Remove with two-stage confirm), needs-attention rows keep "Sign in again"; Add card — Nextcloud + Mail native forms (server/user/password, password field cleared on success or close), and the OAuth row with the mock's honest copy → the existing GNOME dialog handoff (the ALLOWED exception stays for accounts). Errors render as a row, never as page-wide unavailability.

Search & docs (C)

New entries: Fingerprint, Enroll a fingerprint, Delete a user, Account type, Reset a password → users; Nextcloud, Google account, Remove an account, Add a mail account → accounts. Docs regen only if schema changes (none expected — verify).

Contracts (C — write; hermetic stub runs only)

  • user-accounts-contract: extend — keep-files arg end to end, reset-password sends no password material (SetPasswordMode pinned), remove-icon empty-string call, stock list shape, live-validation predicate present in the page (the blur-commit bug regression pin: the add form's fields must not be TextFieldRow).
  • fingerprint-contract: major rework — enrollment is native now (streamed stages against a stubbed fprintd D-Bus layer or a stub helper — follow the hermetic pattern), the GNOME enrollment handoff needle INVERTS (page must NOT call openGnomePanel for users), stuck-state visibility pinned (readerPresent || unlockFeatureEnabled), finger vocabulary single-source kept, set-unlock reason kept.
  • gnome-handoff-contract: delete the UsersPage.qml:system-users ALLOWED exception; the OnlineAccountsPage.qml:online-accounts exception stays with its reason updated to name OAuth specifically.
  • NEW online-accounts-contract: hermetic — availability decoupled from write errors, passwords via stdin never argv, add flows validate inputs, remove requires the confirm state, snapshot exposes no secrets.
  • Backlog Phase 11; README count line (171 → 172 expected).

Agent ownership (parallel)

  • A: scripts/panama-users, scripts/panama-fingerprint, scripts/panama-accounts, services/UserAccounts.qml, services/Fingerprint.qml, services/OnlineAccounts.qml.
  • B: modules/settings/UsersPage.qml, OnlineAccountsPage.qml, new components (+ qmldir); may read AvatarPicker/AvatarCropper and reuse unchanged.
  • C: services/SettingsSearch.qml, contracts above, backlog, README count line.

Hard rules: NO live mutations — no user/password/icon/type changes, no GOA account writes, no authselect, no fprintd enrollment against real hardware (none exists here anyway), no polkit-triggering calls. Read-only probes and stubs only. Valid QML/Python at every save. B programs against the pinned APIs; A updates this spec before changing them.