Sign in with Google, Apple…: remember prompt, item row, new-login picker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -41,6 +41,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
notes: None,
|
||||
favorite: false,
|
||||
tags: vec![],
|
||||
sign_in_with: None,
|
||||
modified_at: 0,
|
||||
})?;
|
||||
let broker = Broker::new(core, Arc::new(Quiet), PairingStore::in_memory());
|
||||
|
||||
@@ -81,6 +81,22 @@ struct Detail {
|
||||
totp: Option<String>,
|
||||
notes: Option<String>,
|
||||
tags: Vec<String>,
|
||||
/// "Google · [email protected]" when the account signs in with a provider.
|
||||
sign_in_with: Option<SignInView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SignInView {
|
||||
provider: String,
|
||||
#[serde(default)]
|
||||
provider_name: String,
|
||||
account: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ProviderView {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -92,6 +108,8 @@ struct NewLogin {
|
||||
totp: Option<String>,
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
sign_in_with: Option<SignInView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -249,9 +267,19 @@ fn get_item(app: State<'_, App>, id: String) -> CmdResult<Detail> {
|
||||
totp: d.totp,
|
||||
notes: d.notes,
|
||||
tags: d.tags,
|
||||
sign_in_with: d.sign_in_with.map(|w| SignInView {
|
||||
provider_name: kv_ffi::sign_in_provider_name(w.provider.clone()),
|
||||
provider: w.provider,
|
||||
account: w.account,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn sign_in_providers() -> Vec<ProviderView> {
|
||||
kv_ffi::sign_in_providers().into_iter().map(|p| ProviderView { id: p.id, name: p.name }).collect()
|
||||
}
|
||||
|
||||
/// Replaces an item's tags (the core normalises them); everything else about the item stays.
|
||||
#[tauri::command]
|
||||
fn set_tags(app: State<'_, App>, id: String, tags: Vec<String>) -> CmdResult<()> {
|
||||
@@ -276,6 +304,7 @@ fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult<String> {
|
||||
notes: None,
|
||||
favorite: false,
|
||||
tags: item.tags,
|
||||
sign_in_with: item.sign_in_with.map(|w| kv_ffi::SignInWithDto { provider: w.provider, account: w.account }),
|
||||
modified_at: 0,
|
||||
})
|
||||
.map_err(err)
|
||||
@@ -701,6 +730,7 @@ fn main() {
|
||||
pending_saves,
|
||||
pending_passkeys,
|
||||
set_tags,
|
||||
sign_in_providers,
|
||||
resolve_passkey,
|
||||
resolve_save,
|
||||
sync_status,
|
||||
|
||||
@@ -38,6 +38,7 @@ fn login(title: &str) -> ItemDto {
|
||||
notes: None,
|
||||
favorite: false,
|
||||
tags: vec![],
|
||||
sign_in_with: None,
|
||||
modified_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ fn pair_and_fill_through_the_native_host() {
|
||||
notes: None,
|
||||
favorite: false,
|
||||
tags: vec![],
|
||||
sign_in_with: None,
|
||||
modified_at: 0,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
+21
-3
@@ -10,10 +10,11 @@ import {
|
||||
|
||||
interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number }
|
||||
interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean; has_totp: boolean; tags: string[] }
|
||||
interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null; tags: string[] }
|
||||
interface SignInView { provider: string; provider_name: string; account: string | null }
|
||||
interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null; tags: string[]; sign_in_with: SignInView | null }
|
||||
interface PairingView { id: string; name: string; caller: string; words: string[] }
|
||||
interface Pairing { id: string; name: string; caller: string; words: string[]; created_at: number }
|
||||
interface SaveView { id: string; origin: string; username: string; update: boolean }
|
||||
interface SaveView { id: string; origin: string; username: string; update: boolean; provider: string | null }
|
||||
interface PasskeyChoice { credential_id: string; user_name: string; title: string }
|
||||
interface PasskeyView { id: string; kind: "get" | "create"; origin: string; rp_id: string; rp_name: string; user_name: string; choices: PasskeyChoice[]; locked: boolean }
|
||||
interface SyncStatus {
|
||||
@@ -630,6 +631,10 @@ async function renderDetail(target: HTMLElement, id: string) {
|
||||
if (item.username) {
|
||||
rows.push(fieldRow("Username", h("span", "", item.username), copyButton("Copy username", () => invoke("copy_secret", { id, field: "username" }))));
|
||||
}
|
||||
if (item.sign_in_with) {
|
||||
const w = item.sign_in_with;
|
||||
rows.push(fieldRow("Sign in with", h("span", "", w.account ? `${w.provider_name} · ${w.account}` : w.provider_name)));
|
||||
}
|
||||
if (item.password) {
|
||||
const value = h("span", "", dots("Password"));
|
||||
let shown = false;
|
||||
@@ -713,13 +718,17 @@ function renderEditor(target: HTMLElement) {
|
||||
const url = input({ type: "url", placeholder: "https://example.com", spellcheck: false });
|
||||
const totp = input({ placeholder: "otpauth://totp/… or base32 secret" }, true);
|
||||
const tags = input({ placeholder: "work, finance", spellcheck: false });
|
||||
const provider = h("select", "kv-field", Object.assign(h("option", "", "Password only"), { value: "" }));
|
||||
void invoke<{ id: string; name: string }[]>("sign_in_providers").then((ps) => provider.append(...ps.map((x) => Object.assign(h("option", "", x.name), { value: x.id }))));
|
||||
const account = input({ placeholder: "Account used with the provider (optional)", spellcheck: false });
|
||||
const gen = button("Generate", "quiet", async () => { pass.value = await invoke<string>("generate_password", { length: 24 }); });
|
||||
const err = errBox();
|
||||
const save = button("Save", "primary");
|
||||
save.onclick = async () => {
|
||||
try {
|
||||
await invoke("save_item", {
|
||||
item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [], totp: totp.value || null, tags: parseTags(tags.value) },
|
||||
item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [], totp: totp.value || null, tags: parseTags(tags.value),
|
||||
sign_in_with: provider.value ? { provider: provider.value, account: account.value.trim() || null } : null },
|
||||
});
|
||||
await render();
|
||||
} catch (e) {
|
||||
@@ -731,6 +740,7 @@ function renderEditor(target: HTMLElement) {
|
||||
field("Title", title), field("Username", user),
|
||||
h("label", "kv-fieldset", h("span", "kv-label", "Password"), h("div", "row", pass, gen)),
|
||||
field("Website", url), field("One-time code", totp, "Optional."), field("Tags", tags, "Optional. Separate tags with commas."),
|
||||
field("Sign in with", provider, "For sites where you use Google, Apple… instead of a password."), field("Provider account", account),
|
||||
err, h("div", "actions", save, button("Cancel", "secondary", () => target.replaceChildren(emptyDetail())))));
|
||||
title.focus();
|
||||
}
|
||||
@@ -1115,6 +1125,14 @@ function saveCard(s: SaveView): HTMLElement {
|
||||
};
|
||||
yes.onclick = () => resolve(true);
|
||||
no.onclick = () => resolve(false);
|
||||
if (s.provider) {
|
||||
yes.textContent = "Remember";
|
||||
return h("div", "kv-sheet prompt",
|
||||
h("h2", "kv-title-2", `Signed in with ${s.provider}?`),
|
||||
p("", `Remember that you sign in to ${s.origin.replace(/^https?:\/\/(www\.)?/, "")} with ${s.provider}${s.username ? ` (${s.username})` : ""}. Kestrove will offer it next time.`),
|
||||
err,
|
||||
h("div", "actions end", no, yes));
|
||||
}
|
||||
return h("div", "kv-sheet prompt",
|
||||
h("h2", "kv-title-2", s.update ? "Update saved password?" : "Save this login?"),
|
||||
h("div", "kv-card rows", h("div", "kv-frow", tile(s.origin.replace(/^https?:\/\/(www\.)?/, "")),
|
||||
|
||||
Reference in New Issue
Block a user