atproto blogging
1mod storage;
2pub use storage::AuthStore;
3
4mod state;
5pub use state::AuthState;
6
7use crate::fetch::Fetcher;
8use dioxus::prelude::*;
9#[cfg(all(feature = "fullstack-server", feature = "server"))]
10use jacquard::oauth::types::OAuthClientMetadata;
11
12/// Result of attempting to restore a session
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RestoreResult {
15 /// Session was successfully restored
16 Restored,
17 /// No saved session was found
18 NoSession,
19 /// Session was found but expired/invalid and has been cleared
20 SessionExpired,
21}
22
23#[cfg(all(feature = "fullstack-server", feature = "server"))]
24#[get("/oauth-client-metadata.json")]
25pub async fn client_metadata() -> Result<axum::Json<serde_json::Value>> {
26 use jacquard::oauth::atproto::atproto_client_metadata;
27
28 use crate::CONFIG;
29
30 let atproto_metadata = atproto_client_metadata(CONFIG.oauth.clone(), &None)?;
31
32 Ok(axum::response::Json(serde_json::to_value(
33 atproto_metadata,
34 )?))
35}
36
37#[cfg(not(target_arch = "wasm32"))]
38pub async fn restore_session(_fetcher: Fetcher, _auth_state: Signal<AuthState>) -> RestoreResult {
39 RestoreResult::NoSession
40}
41
42#[cfg(target_arch = "wasm32")]
43pub async fn restore_session(fetcher: Fetcher, mut auth_state: Signal<AuthState>) -> RestoreResult {
44 use std::collections::BTreeMap;
45
46 use gloo_storage::{LocalStorage, Storage};
47 use jacquard::oauth::authstore::ClientAuthStore;
48 use jacquard::smol_str::SmolStr;
49 use jacquard::types::string::Did;
50
51 // Skip restore if already authenticated (e.g., just completed callback flow)
52 if auth_state.read().is_authenticated() {
53 return RestoreResult::Restored;
54 }
55
56 // Look for session keys in localStorage (format: oauth_session_{did}_{session_id})
57 let entries = match LocalStorage::get_all::<BTreeMap<SmolStr, serde_json::Value>>() {
58 Ok(e) => e,
59 Err(e) => {
60 tracing::warn!("restore_session: localStorage.get_all failed: {:?}", e);
61 return RestoreResult::NoSession;
62 }
63 };
64
65 let mut found_session: Option<(String, String)> = None;
66 for key in entries.keys() {
67 if key.starts_with("oauth_session_") {
68 let parts: Vec<&str> = key
69 .strip_prefix("oauth_session_")
70 .unwrap()
71 .split('_')
72 .collect();
73 if parts.len() >= 2 {
74 found_session = Some((parts[0].to_string(), parts[1..].join("_")));
75 break;
76 }
77 }
78 }
79
80 let Some((did_str, session_id)) = found_session else {
81 return RestoreResult::NoSession;
82 };
83
84 let Ok(did) = Did::new_owned(did_str.clone()) else {
85 tracing::warn!("restore_session: invalid DID format: {}", did_str);
86 return RestoreResult::NoSession;
87 };
88
89 match fetcher.client.oauth_client.restore(&did, &session_id).await {
90 Ok(session) => {
91 let (restored_did, session_id) = session.session_info().await;
92 auth_state
93 .write()
94 .set_authenticated(restored_did, session_id);
95 fetcher.upgrade_to_authenticated(session).await;
96 RestoreResult::Restored
97 }
98 Err(e) => {
99 tracing::warn!("restore_session: failed, clearing dead session: {e}");
100 let _ = AuthStore::new().delete_session(&did, &session_id).await;
101 RestoreResult::SessionExpired
102 }
103 }
104}