feat(gui): list enrolled credentials with per-row remove + enroll button
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
//! "Security keys" preferences page. v1 surface: the current user's enrolled
|
||||
//! FIDO2 credentials with per-row remove + a single "Enroll a new key" button.
|
||||
//! Task 4 lands the empty state + retry banner; Task 5 fills in the list.
|
||||
|
||||
use crate::bus::Daemon;
|
||||
use crate::bus::{current_user, Daemon};
|
||||
use crate::error::user_message;
|
||||
use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
@@ -63,17 +62,112 @@ impl KeysPage {
|
||||
}
|
||||
|
||||
async fn render_connected(self: &Rc<Self>) {
|
||||
// Placeholder until Task 5 fills in the list.
|
||||
let daemon = match self.daemon.borrow().clone() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
self.render_disconnected("internal: daemon handle missing");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let user = current_user();
|
||||
let creds = match daemon.list_credentials(&user).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
self.render_disconnected(&user_message(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let page = adw::PreferencesPage::new();
|
||||
let group = adw::PreferencesGroup::builder()
|
||||
.title("Security keys")
|
||||
.description("FIDO2 keys enrolled for your account")
|
||||
.description(if creds.is_empty() {
|
||||
"No keys enrolled yet. Click below to enroll your first."
|
||||
} else {
|
||||
"FIDO2 keys enrolled for your account"
|
||||
})
|
||||
.build();
|
||||
let placeholder = adw::ActionRow::builder()
|
||||
.title("(Task 5 will populate this)")
|
||||
|
||||
for c in &creds {
|
||||
let title = if c.nickname.is_empty() {
|
||||
c.id.as_str()
|
||||
} else {
|
||||
c.nickname.as_str()
|
||||
};
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(title)
|
||||
.subtitle(short_id(&c.id))
|
||||
.build();
|
||||
let remove = gtk::Button::builder()
|
||||
.icon_name("user-trash-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.css_classes(["flat"])
|
||||
.tooltip_text("Remove this key")
|
||||
.build();
|
||||
let me = self.clone();
|
||||
let cred_id = c.id.clone();
|
||||
remove.connect_clicked(move |_| {
|
||||
let me = me.clone();
|
||||
let cred_id = cred_id.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
me.remove_credential(&cred_id).await;
|
||||
});
|
||||
});
|
||||
row.add_suffix(&remove);
|
||||
group.add(&row);
|
||||
}
|
||||
|
||||
let enroll_group = adw::PreferencesGroup::new();
|
||||
let enroll = adw::ActionRow::builder()
|
||||
.title("Enroll a new key")
|
||||
.activatable(true)
|
||||
.build();
|
||||
group.add(&placeholder);
|
||||
enroll.add_suffix(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
let me = self.clone();
|
||||
enroll.connect_activated(move |_| {
|
||||
let me = me.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
me.start_enroll().await;
|
||||
});
|
||||
});
|
||||
enroll_group.add(&enroll);
|
||||
|
||||
page.add(&group);
|
||||
page.add(&enroll_group);
|
||||
self.root.set_child(Some(&page));
|
||||
}
|
||||
|
||||
async fn remove_credential(self: &Rc<Self>, cred_id: &str) {
|
||||
let daemon = match self.daemon.borrow().clone() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
let user = current_user();
|
||||
if let Err(e) = daemon.remove_own(&user, cred_id).await {
|
||||
self.show_toast(&user_message(&e));
|
||||
return;
|
||||
}
|
||||
self.render_connected().await;
|
||||
}
|
||||
|
||||
async fn start_enroll(self: &Rc<Self>) {
|
||||
// Stub until Task 6 lands the TouchDialog. Until then, surface the
|
||||
// intent so manual smoke can see the click was registered.
|
||||
self.show_toast("Enroll: implemented in Task 6");
|
||||
}
|
||||
|
||||
fn show_toast(self: &Rc<Self>, msg: &str) {
|
||||
// ToastOverlay wiring lands in Task 7. Until then write to stderr so
|
||||
// manual smoke can observe what would be shown.
|
||||
eprintln!("[gui toast] {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
fn short_id(id: &str) -> String {
|
||||
if id.len() > 16 {
|
||||
format!("{}…{}", &id[..8], &id[id.len() - 4..])
|
||||
} else {
|
||||
id.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user