Give identity its due: native enrollment, honest deletion, and sign-in that stays home
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -5,13 +5,19 @@ pragma Singleton
|
||||
// The daemon already runs in this session -- gvfs activates it, and accounts
|
||||
// work without gnome-shell anywhere. Only the panel was GNOME's; the accounts
|
||||
// are D-Bus objects anything may read and modify. So listing, per-service
|
||||
// toggles, and removal all happen here, natively.
|
||||
// toggles, removal, and adding a password account all happen here, natively.
|
||||
//
|
||||
// Signing in is the exception, and only for OAuth providers. The daemon's
|
||||
// AddAccount takes credentials as an argument rather than obtaining them, and
|
||||
// the code that runs Google's OAuth exchange lives in libgoa-backend, which
|
||||
// Fedora ships without a GIR binding. So that one step is handed to GNOME's
|
||||
// panel and the user comes straight back here.
|
||||
// Signing in is the exception, and only for OAuth providers. GOA's AddAccount
|
||||
// takes credentials as an argument rather than obtaining them, which is exactly
|
||||
// what a Nextcloud or IMAP form can supply; what it cannot supply is a Google
|
||||
// token, because the code that runs that exchange lives in libgoa-backend,
|
||||
// which Fedora ships without a GIR binding. So that one step is handed to
|
||||
// GNOME's panel and the user comes straight back here.
|
||||
//
|
||||
// `available` means GOA answered the last time it was asked, and nothing else.
|
||||
// It used to mean "lastError is empty", so a toggle GOA refused turned the
|
||||
// whole page into "Online Accounts is not available" and hid the four working
|
||||
// accounts behind it. A write that failed is a row; it is not the page.
|
||||
//
|
||||
// Read on demand and after every change: accounts are added and removed by
|
||||
// people, not by the system, so there is nothing to poll for.
|
||||
@@ -29,68 +35,141 @@ Singleton {
|
||||
// services: [{key,label,enabled}] }]
|
||||
property var accounts: []
|
||||
property bool scanned: false
|
||||
property bool busy: false
|
||||
property string lastError: ""
|
||||
|
||||
// Whether GOA answered the last snapshot. True to begin with because
|
||||
// nothing has said otherwise yet; `scanned` is what says whether anything
|
||||
// has been asked at all.
|
||||
property bool available: true
|
||||
|
||||
// Kept apart on purpose. The first is why the page might be empty; the
|
||||
// second is why one thing someone just did did not happen. Only the first
|
||||
// has any business deciding whether the page works.
|
||||
property string snapshotError: ""
|
||||
property string writeError: ""
|
||||
|
||||
readonly property string lastError:
|
||||
root.writeError !== "" ? root.writeError : root.snapshotError
|
||||
|
||||
// Guards read the Process objects; this is only for the page to bind to,
|
||||
// so rows can go quiet while a change is in flight.
|
||||
readonly property bool busy: list.running || write.running || add.running
|
||||
|
||||
// Accounts whose stored credentials have stopped working -- an expired
|
||||
// token, a changed password. GOA knows, and nothing outside its own panel
|
||||
// ever says so, which is how an account quietly stops syncing for weeks.
|
||||
readonly property int attentionCount: root.accounts.filter(a => a.needsAttention).length
|
||||
|
||||
readonly property bool available: root.lastError === ""
|
||||
|
||||
// Cheap enough for every page open: one short-lived process reading D-Bus
|
||||
// objects that are already in memory.
|
||||
function refresh(): void {
|
||||
if (!list.running)
|
||||
list.running = true;
|
||||
}
|
||||
|
||||
function absorb(text: string): void {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
root.accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
|
||||
root.snapshotError = String(parsed.error ?? "");
|
||||
root.available = root.snapshotError === "";
|
||||
} catch (error) {
|
||||
root.accounts = [];
|
||||
root.snapshotError = "Could not read the accounts helper's output.";
|
||||
root.available = false;
|
||||
console.warn("OnlineAccounts: could not parse helper output:", error);
|
||||
}
|
||||
root.scanned = true;
|
||||
}
|
||||
|
||||
// Enabling a service clears GOA's "disabled" flag; the helper owns that
|
||||
// inversion so the UI can speak in terms of what is on.
|
||||
function setService(path: string, service: string, enabled: bool): void {
|
||||
if (root.busy)
|
||||
if (write.running)
|
||||
return;
|
||||
root.busy = true;
|
||||
root.writeError = "";
|
||||
write.command = [root.helperPath, "set", path, service, enabled ? "true" : "false"];
|
||||
write.running = true;
|
||||
}
|
||||
|
||||
function remove(path: string): void {
|
||||
if (root.busy)
|
||||
if (write.running)
|
||||
return;
|
||||
root.busy = true;
|
||||
root.writeError = "";
|
||||
write.command = [root.helperPath, "remove", path];
|
||||
write.running = true;
|
||||
}
|
||||
|
||||
// ── Adding a password account ────────────────────────────────────────────
|
||||
//
|
||||
// The password goes to the helper's stdin and nowhere else: never an
|
||||
// argument, because argv is readable by every process on this machine.
|
||||
// It is written from onStarted, because a process has no stdin to write to
|
||||
// until it is running -- the same pattern UserAccounts uses for a new
|
||||
// password, and HomeAssistantConfig for its token.
|
||||
property string pendingPassword: ""
|
||||
|
||||
function addNextcloud(server: string, user: string, password: string): void {
|
||||
root.beginAdd(["add-nextcloud", server, user], password);
|
||||
}
|
||||
|
||||
function addImap(email: string, imapHost: string, smtpHost: string,
|
||||
user: string, password: string): void {
|
||||
root.beginAdd(["add-imap", email, imapHost, smtpHost, user], password);
|
||||
}
|
||||
|
||||
function beginAdd(arguments: var, password: string): void {
|
||||
if (add.running)
|
||||
return;
|
||||
root.writeError = "";
|
||||
root.pendingPassword = password;
|
||||
add.command = [root.helperPath].concat(arguments);
|
||||
add.stdinEnabled = true;
|
||||
add.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: list
|
||||
command: [root.helperPath, "list"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
|
||||
root.lastError = String(parsed.error ?? "");
|
||||
} catch (error) {
|
||||
root.accounts = [];
|
||||
root.lastError = "Could not read the accounts helper's output.";
|
||||
console.warn("OnlineAccounts: could not parse helper output:", error);
|
||||
}
|
||||
root.scanned = true;
|
||||
}
|
||||
}
|
||||
command: [root.helperPath, "snapshot"]
|
||||
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
|
||||
}
|
||||
|
||||
Process {
|
||||
id: write
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
|
||||
onStreamFinished: if (this.text.trim() !== "") root.writeError = this.text.trim();
|
||||
}
|
||||
// Re-read rather than assuming the write landed: GOA may refuse, and a
|
||||
// toggle that sprang back is the honest outcome.
|
||||
onExited: {
|
||||
root.busy = false;
|
||||
root.refresh();
|
||||
onExited: root.refresh()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: add
|
||||
stdinEnabled: true
|
||||
onStarted: {
|
||||
add.write(root.pendingPassword + "\n");
|
||||
// Held for as long as it takes to hand over, and no longer.
|
||||
root.pendingPassword = "";
|
||||
add.stdinEnabled = false;
|
||||
}
|
||||
// The helper answers with the fresh account list plus whatever went
|
||||
// wrong, so a successful add lands on the page without a second read.
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.accounts = Array.isArray(parsed.accounts) ? parsed.accounts : root.accounts;
|
||||
// An add that failed says why here. It is a write error:
|
||||
// GOA answered, so the page is still working.
|
||||
root.writeError = String(parsed.error ?? "");
|
||||
} catch (error) {
|
||||
root.writeError = "Could not read the accounts helper's output.";
|
||||
}
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: if (this.text.trim() !== "") root.writeError = this.text.trim();
|
||||
}
|
||||
onExited: root.pendingPassword = ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user