web: add account passkey login UI

This commit is contained in:
2026-07-23 03:14:43 +09:00
parent 7dda898830
commit 7740191194
12 changed files with 760 additions and 4 deletions
+39 -2
View File
@@ -38,8 +38,8 @@ use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeE
use crate::auth::{
AuthPublicConfig, AuthenticatedUser, RequestActor, auth_error, is_expired, mint_secret, new_id,
new_user_code, normalize_handle, resolve_request_actor, rfc3339_after, session_set_cookie,
token_hash,
new_user_code, normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after,
session_set_cookie, token_hash,
};
use crate::companion::{
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
@@ -337,6 +337,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
"/api/auth/passkeys/login/complete",
post(post_passkey_login_complete),
)
.route("/api/auth/logout", post(post_auth_logout))
.route("/api/auth/device-login/start", post(post_device_login_start))
.route("/api/auth/device-login/approve", post(post_device_login_approve))
.route("/api/auth/device-login/poll", post(post_device_login_poll))
@@ -2595,6 +2596,11 @@ struct WhoamiResponse {
actor: Option<RequestActor>,
}
#[derive(Debug, Serialize, Deserialize)]
struct LogoutResponse {
status: String,
}
async fn get_auth_config(State(api): State<WorkspaceApi>) -> ApiResult<Json<AuthPublicConfig>> {
Ok(Json(auth_public_config(&api.config)))
}
@@ -3023,6 +3029,37 @@ async fn get_auth_whoami(
}))
}
async fn post_auth_logout(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
) -> ApiResult<Response> {
let auth = auth_public_config(&api.config);
if let Some(session_token) = parse_cookie(&headers, &auth.cookie_name) {
let _ = api
.store
.revoke_browser_session(&token_hash(&session_token), &crate::auth::now_rfc3339())?;
}
let mut response_headers = HeaderMap::new();
response_headers.insert(
SET_COOKIE,
session_set_cookie(&auth.cookie_name, "", 0)
.parse()
.map_err(|error| {
auth_error(
"invalid_session_cookie",
&format!("failed to build logout cookie: {error}"),
)
})?,
);
Ok((
response_headers,
Json(LogoutResponse {
status: "logged_out".to_string(),
}),
)
.into_response())
}
fn webauthn(config: &ServerConfig) -> ApiResult<Webauthn> {
let auth = auth_public_config(config);
let origin = Url::parse(&auth.origin)
+36
View File
@@ -243,6 +243,7 @@ pub trait ControlPlaneStore: Send + Sync {
) -> Result<Option<AuthChallengeRecord>>;
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()>;
fn resolve_browser_session(&self, token_hash: &str) -> Result<Option<BrowserSessionRecord>>;
fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result<bool>;
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()>;
fn resolve_api_token(&self, token_hash: &str) -> Result<Option<ApiTokenRecord>>;
fn mark_api_token_used(&self, token_hash: &str, used_at: &str) -> Result<()>;
@@ -621,6 +622,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
})
}
fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result<bool> {
self.with_conn(|conn| {
let changed = conn.execute(
"UPDATE browser_sessions SET revoked_at = ?2 WHERE token_hash = ?1 AND revoked_at IS NULL",
params![token_hash, revoked_at],
)?;
Ok(changed > 0)
})
}
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()> {
self.with_conn(|conn| {
conn.execute(
@@ -2455,6 +2466,31 @@ mod tests {
None
);
let browser_session = BrowserSessionRecord {
session_id: "session-id".to_string(),
token_hash: "session-hash".to_string(),
user_id: user.user_id.clone(),
created_at: now.clone(),
expires_at: "2026-07-22T12:00:00Z".to_string(),
revoked_at: None,
};
store.create_browser_session(&browser_session).unwrap();
assert_eq!(
store.resolve_browser_session("session-hash").unwrap(),
Some(browser_session)
);
assert!(
store
.revoke_browser_session("session-hash", "2026-07-22T00:02:00Z")
.unwrap()
);
assert_eq!(store.resolve_browser_session("session-hash").unwrap(), None);
assert!(
!store
.revoke_browser_session("session-hash", "2026-07-22T00:03:00Z")
.unwrap()
);
let api_token = ApiTokenRecord {
token_id: "token-id".to_string(),
token_hash: "hash".to_string(),