feat(auth): add WebAuthn passkey support with migrations, routes, and tests

This commit is contained in:
2025-10-29 17:50:14 +01:00
parent ec6e7f04f2
commit ca30b20873
23 changed files with 1511 additions and 83 deletions
+14 -24
View File
@@ -49,7 +49,6 @@ enum Command {
AddUserToTenant {
username: String,
slug: String,
role: Option<String>,
},
RemoveUserFromTenant {
username: String,
@@ -73,7 +72,7 @@ impl Command {
delete-user <username>\n\
create-tenant <slug> [storage_root] [quickwit_index]\n\
delete-tenant <slug>\n\
add-user-to-tenant <username> <slug> [role]\n\
add-user-to-tenant <username> <slug>\n\
remove-user-from-tenant <username> <slug>\n\
reanalyze-documents <slug>\n\
list-tenants\n\
@@ -108,7 +107,6 @@ impl Command {
Some("add-user-to-tenant") => Ok(Self::AddUserToTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?,
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?,
role: args.next(),
}),
Some("remove-user-from-tenant") => Ok(Self::RemoveUserFromTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?,
@@ -150,11 +148,7 @@ async fn main() -> Result<()> {
quickwit_index,
} => create_tenant(&pool, &slug, storage_root, quickwit_index)?,
Command::DeleteTenant { slug } => delete_tenant(&pool, &slug)?,
Command::AddUserToTenant {
username,
slug,
role,
} => add_user_to_tenant(&pool, &username, &slug, role.as_deref())?,
Command::AddUserToTenant { username, slug } => add_user_to_tenant(&pool, &username, &slug)?,
Command::RemoveUserFromTenant { username, slug } => {
remove_user_from_tenant(&pool, &username, &slug)?
}
@@ -232,21 +226,22 @@ fn list_users(pool: &PgPool) -> Result<()> {
}
for user in users_list {
let memberships: Vec<(Uuid, String, String)> = user_memberships::table
let memberships: Vec<String> = user_memberships::table
.inner_join(tenants::table)
.filter(user_memberships::user_id.eq(user.id))
.select((tenants::id, tenants::slug, user_memberships::role))
.order((tenants::slug.asc(), user_memberships::role.asc()))
.select(tenants::slug)
.order(tenants::slug.asc())
.load(&mut conn)?;
if memberships.is_empty() {
println!("{} ({})", user.username, user.id);
} else {
let details: Vec<String> = memberships
.into_iter()
.map(|(_, slug, role)| format!("{}: {}", slug, role))
.collect();
println!("{} ({}) -> {}", user.username, user.id, details.join(", "));
println!(
"{} ({}) -> {}",
user.username,
user.id,
memberships.join(", ")
);
}
}
@@ -324,7 +319,7 @@ fn delete_tenant(pool: &PgPool, slug: &str) -> Result<()> {
Ok(())
}
fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str, role: Option<&str>) -> Result<()> {
fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
let user: User = users::table
@@ -343,20 +338,15 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str, role: Option<&s
id: Uuid::new_v4(),
user_id: user.id,
tenant_id: tenant.id,
role: role.unwrap_or("user").to_string(),
};
diesel::insert_into(user_memberships::table)
.values(&membership)
.on_conflict((user_memberships::user_id, user_memberships::tenant_id))
.do_update()
.set(user_memberships::role.eq(&membership.role))
.do_nothing()
.execute(&mut conn)?;
println!(
"added user '{}' to tenant '{}' with role '{}'",
username, slug, membership.role
);
println!("added user '{}' to tenant '{}'", username, slug);
Ok(())
}