learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1use crate::state::SharedState;
2
3use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6
7#[derive(Deserialize)]
8pub struct LoginRequest {
9 identifier: String,
10 password: String,
11}
12
13#[derive(Serialize)]
14pub struct LoginResponse {
15 access_jwt: String,
16 refresh_jwt: String,
17 did: String,
18 handle: String,
19}
20
21/// Login with app password.
22///
23/// Resolves the user's PDS from their handle/DID, authenticates with that PDS,
24/// and stores the session for future requests.
25pub async fn login(State(state): State<SharedState>, Json(payload): Json<LoginRequest>) -> impl IntoResponse {
26 use crate::oauth::resolver::{is_valid_did, is_valid_handle};
27 use crate::repository::oauth::StoreAppPasswordSessionRequest;
28
29 let client = reqwest::Client::new();
30
31 let pds_url = if is_valid_did(&payload.identifier) {
32 match state.identity_resolver.resolve_did(&payload.identifier).await {
33 Ok(identity) => identity.pds_url,
34 Err(e) => {
35 tracing::error!("Failed to resolve DID {}: {}", payload.identifier, e);
36 return (
37 StatusCode::BAD_REQUEST,
38 Json(json!({ "error": format!("Failed to resolve DID: {}", e) })),
39 )
40 .into_response();
41 }
42 }
43 } else if is_valid_handle(&payload.identifier) {
44 let did = match state.identity_resolver.resolve_handle(&payload.identifier).await {
45 Ok(did) => did,
46 Err(e) => {
47 tracing::error!("Failed to resolve handle {}: {}", payload.identifier, e);
48 return (
49 StatusCode::BAD_REQUEST,
50 Json(json!({ "error": format!("Failed to resolve handle: {}", e) })),
51 )
52 .into_response();
53 }
54 };
55
56 match state.identity_resolver.resolve_did(&did).await {
57 Ok(identity) => identity.pds_url,
58 Err(e) => {
59 tracing::error!("Failed to resolve DID {} for handle {}: {}", did, payload.identifier, e);
60 return (
61 StatusCode::BAD_REQUEST,
62 Json(json!({ "error": format!("Failed to resolve DID: {}", e) })),
63 )
64 .into_response();
65 }
66 }
67 } else {
68 tracing::warn!("Invalid identifier format: {}, using default PDS", payload.identifier);
69 state.config.pds_url.clone()
70 };
71
72 tracing::info!("Authenticating {} with PDS: {}", payload.identifier, pds_url);
73
74 let resp = client
75 .post(format!("{}/xrpc/com.atproto.server.createSession", pds_url))
76 .json(&json!({
77 "identifier": payload.identifier,
78 "password": payload.password
79 }))
80 .send()
81 .await;
82
83 match resp {
84 Ok(response) => {
85 if response.status().is_success() {
86 let body: serde_json::Value = response.json().await.unwrap_or_default();
87 let access_jwt = body["accessJwt"].as_str().unwrap_or("").to_string();
88 let refresh_jwt = body["refreshJwt"].as_str().unwrap_or("").to_string();
89 let did = body["did"].as_str().unwrap_or("").to_string();
90 let handle = body["handle"].as_str().unwrap_or("").to_string();
91
92 // Store the session with the resolved PDS URL
93 let store_result = state
94 .oauth_repo
95 .store_app_password_session(StoreAppPasswordSessionRequest {
96 did: &did,
97 pds_url: &pds_url,
98 access_token: &access_jwt,
99 refresh_token: Some(&refresh_jwt),
100 expires_at: None,
101 })
102 .await;
103
104 if let Err(e) = store_result {
105 tracing::error!("Failed to store app password session: {}", e);
106 }
107
108 (
109 StatusCode::OK,
110 Json(json!({
111 "accessJwt": access_jwt,
112 "refreshJwt": refresh_jwt,
113 "did": did,
114 "handle": handle
115 })),
116 )
117 .into_response()
118 } else {
119 let error_body: serde_json::Value = response.json().await.unwrap_or_default();
120 (StatusCode::UNAUTHORIZED, Json(error_body)).into_response()
121 }
122 }
123 Err(e) => (
124 StatusCode::INTERNAL_SERVER_ERROR,
125 Json(json!({ "error": e.to_string() })),
126 )
127 .into_response(),
128 }
129}
130
131pub async fn me(ctx: Option<axum::Extension<crate::middleware::auth::UserContext>>) -> impl IntoResponse {
132 match ctx {
133 Some(axum::Extension(user)) => (
134 StatusCode::OK,
135 Json(json!({
136 "status": "authenticated",
137 "did": user.did,
138 "handle": user.handle
139 })),
140 )
141 .into_response(),
142 None => (StatusCode::UNAUTHORIZED, Json(json!({ "error": "Unauthorized" }))).into_response(),
143 }
144}