12 KiB
Users & Accounts redesign — identity, honestly
Approved mock: home-mocks/users.html (scratchpad, :8642). Spec wins over mock on conflict.
Goals
- 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.
- 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.
- 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-avatarsprints its own shape:{"avatars":[{"name","path"}],"error":""}. Absolute paths, sorted by name; a missing/usr/share/pixmaps/facesyields[]. Fedora ships 15.delete-usertakeskeep|remove; the olderkeep-files|remove-filesspelling still works.reset-password USER→SetPasswordMode(1). No password material anywhere in the path.set-account-typenow carries the last-admin refusal in the helper (same sentence asdelete-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}.pamEnabledandunlockFeatureEnabledare the same fact under the old and new name. Behaviour change for C: authselect is now parsed unconditionally, so a readerless machine reportspamEnabled: truewhen the feature is on. That used to be forced tofalse, which is what made the stuck state unreachable. (This machine is in it.)enroll FINGERstreams one JSON object per line:{ok, stage, done, total, result, error}withstage ∈ claiming|scanning|done|failed. fprintdClaim→EnrollStart→ one line perEnrollStatus→EnrollStop→Release. Cancellation is SIGTERM (stopping the Process); the helper still releases the device. Exit 0 onenroll-completed, 1 otherwise. 90s idle watchdog.remove FINGER/remove-all→DeleteEnrolledFinger/DeleteEnrolledFingers2under a Claim, and answer with thestatusshape 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 toPANAMA_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}) → ois fully daemon-side — goa-daemon writesaccounts.confand callsgoa_utils_store_credentials_for_id_syncitself. The missing libgoa-backend GIR blocks only the client-side sign-in dialog, so password providers are genuinely addable and OAuth genuinely is not.IsSupportedProvideris checked first (owncloud,imap_smtpboth true here;nextcloudis not a provider type —owncloudis). - Keys are GOA 3.58's own, taken from
goaowncloudprovider.c/goaimapsmtpprovider.cand confirmed against this machine'saccounts.conf. Nextcloud: credentials{password}, detailsUri(…/remote.php/webdav/),FilesEnabled,CalendarEnabled,CalDavUri(…/remote.php/dav/),ContactsEnabled,CardDavUri,AcceptSslErrors. IMAP: credentials{imap-password, smtp-password}, detailsEnabled,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
EnsureCredentialson the new object and removes the account again if the credentials are refused, reporting why. - Fixture seam:
PANAMA_ACCOUNTS_FIXTURE= a JSON file inlist's shape. With it set nothing imports gi; validation still runs; each verb appends{"verb","arguments","passwordBytes"}toPANAMA_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 oldremoveFiles—resetPassword(user),setAccountTypeFor(user, type)(renamed fromsetAccountType),setLocked(user, locked).lockedandloginTimestay in the payload for the UI. - Fingerprint:
readerPresent,readerName,unlockFeatureEnabled,fingers(renamed fromenrolled),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/writeErrorand the derivedlastError,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 theUsersPage.qml:system-usersALLOWED exception; theOnlineAccountsPage.qml:online-accountsexception 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.