pharos_native/runtime/social_connections/
consent_ui.rs1use super::*;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub(super) enum Outcome {
7 Cancelled,
8 Failed,
9 SelectionRequired,
10}
11
12type ActorKey = (String, String, i64, String, String);
13type Feedback = BTreeMap<ActorKey, (Outcome, std::time::Instant)>;
14
15fn key(scope: &ConnectionScope, provider: SocialProvider, session: &str) -> ActorKey {
17 (
18 scope.application().into(),
19 scope.namespace().into(),
20 scope.owner(),
21 provider.key().into(),
22 session.into(),
23 )
24}
25
26fn feedback() -> &'static std::sync::Mutex<Feedback> {
28 static FEEDBACK: std::sync::OnceLock<std::sync::Mutex<Feedback>> = std::sync::OnceLock::new();
29 FEEDBACK.get_or_init(|| std::sync::Mutex::new(BTreeMap::new()))
30}
31
32pub(super) fn record(
34 scope: &ConnectionScope,
35 provider: SocialProvider,
36 session: &str,
37 outcome: Option<Outcome>,
38) {
39 if let Ok(mut entries) = feedback().lock() {
40 entries.retain(|_, (_, created)| created.elapsed() < Duration::from_secs(900));
41 let key = key(scope, provider, session);
42 entries.remove(&key);
43 if let Some(outcome) = outcome {
44 if entries.len() >= 128
45 && let Some(oldest) = entries
46 .iter()
47 .min_by_key(|(_, (_, time))| *time)
48 .map(|(key, _)| key.clone())
49 {
50 entries.remove(&oldest);
51 }
52 entries.insert(key, (outcome, std::time::Instant::now()));
53 }
54 }
55}
56
57pub(super) fn outcome(
59 scope: &ConnectionScope,
60 provider: SocialProvider,
61 session: &str,
62) -> Option<Outcome> {
63 feedback()
64 .lock()
65 .ok()?
66 .get(&key(scope, provider, session))
67 .filter(|(_, created)| created.elapsed() < Duration::from_secs(900))
68 .map(|(outcome, _)| *outcome)
69}
70
71pub(super) fn pending(scope: &ConnectionScope, provider: SocialProvider, session: &str) -> bool {
73 pending_registry().lock().is_ok_and(|entries| {
74 entries.values().any(|attempt| {
75 attempt.scope == *scope
76 && attempt.provider == provider
77 && attempt.session == session
78 && attempt.created.elapsed() < Duration::from_secs(300)
79 })
80 })
81}
82
83pub(super) fn claim_guard(
85 key: &str,
86 scope: &ConnectionScope,
87 provider: SocialProvider,
88 session: &str,
89) -> Result<std::sync::MutexGuard<'static, BTreeMap<String, Pending>>, SocialError> {
90 let registry = pending_registry().lock().map_err(|_| SocialError::Busy)?;
91 if !registry.get(key).is_some_and(|attempt| {
92 attempt.scope == *scope
93 && attempt.provider == provider
94 && attempt.session == session
95 && attempt.attempt.is_none()
96 }) {
97 return Err(SocialError::AuthorizationRequired);
98 }
99 Ok(registry)
100}
101
102pub(super) fn cancel(
104 scope: &ConnectionScope,
105 provider: SocialProvider,
106 session: &str,
107) -> Result<(), SocialError> {
108 let mut removed = false;
109 pending_registry()
110 .lock()
111 .map_err(|_| SocialError::Busy)?
112 .retain(|_, attempt| {
113 let keep = attempt.scope != *scope
114 || attempt.provider != provider
115 || attempt.session != session;
116 removed |= !keep;
117 keep
118 });
119 if removed {
120 record(scope, provider, session, Some(Outcome::Cancelled));
121 }
122 Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 #[test]
130 fn feedback_is_scoped_and_persists_until_the_next_attempt() {
131 let scope = ConnectionScope::new("consent-ui", "deployment", 7).unwrap();
132 record(
133 &scope,
134 SocialProvider::Facebook,
135 "session-a",
136 Some(Outcome::Failed),
137 );
138 assert_eq!(
139 outcome(&scope, SocialProvider::Facebook, "session-a"),
140 Some(Outcome::Failed)
141 );
142 for (scope, provider, session) in [
143 (&scope, SocialProvider::Facebook, "session-b"),
144 (&scope, SocialProvider::X, "session-a"),
145 (
146 &ConnectionScope::new("consent-ui", "another", 7).unwrap(),
147 SocialProvider::Facebook,
148 "session-a",
149 ),
150 ] {
151 assert!(outcome(scope, provider, session).is_none());
152 }
153 record(&scope, SocialProvider::Facebook, "session-a", None);
154 assert!(outcome(&scope, SocialProvider::Facebook, "session-a").is_none());
155 }
156}