1use std::sync::Arc;
12
13use crate::backend::storage::store::{IdentityAccount, SecretRef, Store};
14use crate::backend::mps::Broker;
15
16fn persist_oauth_direct_account(
34 mstore: &Arc<Store>,
35 identity_store: &Arc<Store>,
36 broker: &Arc<Broker>,
37 account_id: &str,
38 provider_id: &str,
39 dir: Option<&str>,
40 _session_id: &str,
41 email: Option<&str>,
42) -> Option<String> {
43 let dir = match dir.filter(|s| !s.is_empty()) {
44 Some(d) => d,
45 None => {
46 tracing::warn!(
47 target: "identity",
48 account_id,
49 provider_id,
50 "auth success (direct-account): dir unresolved — skipping account persist"
51 );
52 return None;
53 }
54 };
55 let now = agentmux_common::time::now_ms();
56 let account = IdentityAccount {
57 id: account_id.to_string(),
58 name: format!("{provider_id}-oauth"),
59 provider: provider_id.to_string(),
60 kind: "oauth".to_string(),
61 display_name: String::new(),
66 secret_ref: SecretRef::OAuthConfigDir { dir: dir.to_string() },
67 context: match email
73 .map(str::trim)
74 .filter(|e| !e.is_empty())
75 .map(str::to_string)
76 .or_else(|| crate::identity::account_email::email_from_oauth_dir(provider_id, dir))
77 {
78 Some(e) => serde_json::json!({ "email": e }),
79 None => serde_json::json!({}),
80 },
81 status: crate::identity::resolver::oauth_status::VALID.to_string(),
84 created_at: now,
85 updated_at: now,
86 };
87 if let Err(e) = mstore.identity_upsert_with_mirror(identity_store, &account) {
92 tracing::warn!(
93 target: "identity",
94 account_id,
95 provider_id,
96 error = %e,
97 "auth success (direct-account): identity_upsert failed"
98 );
99 return None;
100 }
101 broker.publish_event::<crate::backend::mps_events::IdentityAccountsChanged>(&(), vec![], 0);
102 tracing::info!(
103 target: "identity",
104 account_id,
105 provider_id,
106 dir,
107 "auth success (direct-account): OAuth account persisted"
108 );
109 Some(account_id.to_string())
110}
111
112#[allow(clippy::too_many_arguments)]
133pub(crate) fn persist_oauth_success(
134 mstore: &Arc<Store>,
135 identity_store: &Arc<Store>,
136 broker: &Arc<Broker>,
137 _direct_account: bool,
138 account_id: &str,
139 _into_bundle_id: Option<&str>,
140 provider_id: &str,
141 dir: Option<&str>,
142 session_id: &str,
143 email: Option<&str>,
144) -> (String, Option<String>) {
145 if account_id.is_empty() {
146 return (String::new(), None);
147 }
148 let persisted = persist_oauth_direct_account(mstore, identity_store, broker, account_id, provider_id, dir, session_id, email);
149 (String::new(), persisted)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn persist_oauth_direct_account_round_trip() {
158 let mstore = Arc::new(Store::open_in_memory().unwrap());
159 let identity_store = Arc::new(Store::open_in_memory().unwrap());
160 let broker = Arc::new(crate::backend::mps::Broker::new());
161 let r = persist_oauth_direct_account(
162 &mstore,
163 &identity_store,
164 &broker,
165 "acc-1",
166 "claude",
167 Some("/some/account/dir"),
168 "sess-z",
169 None,
170 );
171 assert_eq!(r, Some("acc-1".to_string()));
172
173 let acct = mstore.identity_get("acc-1").unwrap().expect("account row exists");
174 assert_eq!(acct.provider, "claude");
175 assert_eq!(acct.kind, "oauth");
176 assert_eq!(acct.status, "valid");
177 match acct.secret_ref {
178 SecretRef::OAuthConfigDir { dir } => assert_eq!(dir, "/some/account/dir"),
179 other => panic!("expected OAuthConfigDir, got {other:?}"),
180 }
181 }
182
183 #[test]
184 fn persist_oauth_direct_account_returns_none_when_dir_unresolved() {
185 let mstore = Arc::new(Store::open_in_memory().unwrap());
186 let identity_store = Arc::new(Store::open_in_memory().unwrap());
187 let broker = Arc::new(crate::backend::mps::Broker::new());
188 let r = persist_oauth_direct_account(&mstore, &identity_store, &broker, "acc-1", "claude", None, "sess-z", None);
189 assert!(r.is_none());
190 assert!(mstore.identity_get("acc-1").unwrap().is_none(), "nothing persisted when dir is unresolved");
191 }
192
193 #[test]
198 fn persist_records_the_login_email_in_context_leaving_display_name_free() {
199 let mstore = Arc::new(Store::open_in_memory().unwrap());
200 let identity_store = Arc::new(Store::open_in_memory().unwrap());
201 let broker = Arc::new(crate::backend::mps::Broker::new());
202 persist_oauth_direct_account(
203 &mstore,
204 &identity_store,
205 &broker,
206 "acc-1",
207 "claude",
208 Some("/dir"),
209 "sess-z",
210 Some("user@example.com"),
211 );
212 let acct = mstore.identity_get("acc-1").unwrap().expect("account row");
213 assert_eq!(acct.context["email"], "user@example.com");
214 assert_eq!(acct.display_name, "", "display_name stays the user's to set");
215 }
216
217 #[test]
221 fn persist_omits_the_email_key_entirely_when_none_was_captured() {
222 let mstore = Arc::new(Store::open_in_memory().unwrap());
223 let identity_store = Arc::new(Store::open_in_memory().unwrap());
224 let broker = Arc::new(crate::backend::mps::Broker::new());
225 for email in [None, Some(""), Some(" ")] {
226 let id = format!("acc-{}", email.unwrap_or("none").len());
227 persist_oauth_direct_account(
228 &mstore, &identity_store, &broker, &id, "claude", Some("/dir"), "sess-z", email,
229 );
230 let acct = mstore.identity_get(&id).unwrap().expect("account row");
231 assert!(
232 acct.context.get("email").is_none(),
233 "blank email {email:?} must not create the key"
234 );
235 }
236 }
237
238 #[test]
239 fn persist_oauth_success_always_routes_direct_account_mode() {
240 let mstore = Arc::new(Store::open_in_memory().unwrap());
245 let identity_store = Arc::new(Store::open_in_memory().unwrap());
246 let broker = Arc::new(crate::backend::mps::Broker::new());
247 let (bundle_id, account_id) = persist_oauth_success(
248 &mstore,
249 &identity_store,
250 &broker,
251 true,
252 "acc-1",
253 None,
254 "claude",
255 Some("/some/dir"),
256 "sess-route",
257 None,
258 );
259 assert_eq!(bundle_id, "", "bundle id is always empty now");
260 assert_eq!(account_id, Some("acc-1".to_string()));
261 assert!(mstore.identity_get("acc-1").unwrap().is_some());
262 }
263
264 #[test]
265 fn persist_oauth_success_skips_persistence_when_account_id_is_empty() {
266 let mstore = Arc::new(Store::open_in_memory().unwrap());
273 let identity_store = Arc::new(Store::open_in_memory().unwrap());
274 let broker = Arc::new(crate::backend::mps::Broker::new());
275 let (bundle_id, account_id) = persist_oauth_success(
276 &mstore,
277 &identity_store,
278 &broker,
279 false,
280 "",
281 None,
282 "claude",
283 Some("/some/dir"),
284 "sess-empty",
285 None,
286 );
287 assert_eq!(bundle_id, "");
288 assert_eq!(account_id, None, "empty account_id must not be persisted");
289 assert!(
290 mstore.identity_get("").unwrap().is_none(),
291 "no row with id=\"\" should ever be written"
292 );
293 }
294}