add passkey support
ci/woodpecker/push/e2e Pipeline failed
ci/woodpecker/push/fmt Pipeline was successful
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
2026-08-02 16:46:52 -04:00
parent 04c9724c76
commit fdd3e8c5da
18 changed files with 1073 additions and 28 deletions
+50 -2
View File
@@ -3,7 +3,7 @@ use pulldown_cmark::{Options, Parser, html as cmark_html};
use crate::{
domain::PresenceUser,
domain::{Category, GroceryList, Item, Meal, MealIngredient, User},
domain::{Category, GroceryList, Item, Meal, MealIngredient, Passkey, User},
};
pub fn login_page(error: Option<&str>, invite: Option<&str>) -> Markup {
@@ -28,8 +28,11 @@ pub fn login_page(error: Option<&str>, invite: Option<&str>) -> Markup {
input id="password" name="password" type="password" autocomplete="current-password" required;
button class="button button-primary" type="submit" { "Sign in" }
}
div class="auth-divider" { span { "or" } }
button id="passkey-login" class="button button-secondary" type="button" { "Sign in with a passkey" }
p class="auth-switch" { "Need an account? " a href="/register" { "Create one" } }
}
script src="/static/passkey-login.js" {}
},
)
}
@@ -79,6 +82,51 @@ pub fn registration_closed_page() -> Markup {
)
}
pub fn account_page(user: &User, passkeys: &[Passkey], csrf_token: &str) -> Markup {
page(
"Account",
Some(user),
html! {
div class="page-heading" {
div {
p class="eyebrow" { "ACCOUNT" }
h1 { "Account" }
p class="lede" { "Manage your sign-in methods." }
}
}
div class="dashboard-grid" {
section class="panel" {
div class="panel-heading" {
h2 { "Passkeys" }
span class="count-badge" { (passkeys.len()) }
}
p { "Passkeys let you sign in without a password using your device." }
@if passkeys.is_empty() {
p class="muted" { "You have no passkeys yet." }
} @else {
div class="passkey-list" {
@for passkey in passkeys {
div class="passkey-row" {
div class="item-copy" {
strong { "Passkey" }
small { (passkey.credential_id) }
}
form method="post" action=(format!("/account/passkeys/{}/delete", passkey.id)) {
input type="hidden" name="csrf" value=(csrf_token);
button class="danger-link" type="submit" { "Remove" }
}
}
}
}
}
button id="add-passkey" class="button button-primary" type="button" data-csrf=(csrf_token) { "Add a passkey" }
}
}
script src="/static/passkey-register.js" {}
},
)
}
pub fn lists_page(user: &User, lists: &[GroceryList], csrf_token: &str) -> Markup {
page(
"Your lists",
@@ -907,7 +955,7 @@ fn page(title: &str, user: Option<&User>, content: Markup) -> Markup {
a href="/meals" { "Meals" }
}
div class="account-nav" {
span class="user-name" { (user.display_name) }
a class="user-name" href="/account" { (user.display_name) }
form method="post" action="/logout" {
button class="text-button" type="submit" { "Sign out" }
}