Skip to main content

authx_storage/memory/
mod.rs

1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3
4#[cfg(test)]
5mod tests;
6
7use async_trait::async_trait;
8use chrono::Utc;
9use uuid::Uuid;
10
11use authx_core::{
12    error::{AuthError, Result, StorageError},
13    models::{
14        ApiKey, AuditLog, AuthorizationCode, CreateApiKey, CreateAuditLog, CreateAuthorizationCode,
15        CreateCredential, CreateDeviceCode, CreateInvite, CreateOidcClient,
16        CreateOidcFederationProvider, CreateOidcToken, CreateOrg, CreateSession, CreateUser,
17        Credential, CredentialKind, DeviceCode, Invite, Membership, OAuthAccount, OidcClient,
18        OidcFederationProvider, OidcToken, Organization, Role, Session, UpdateUser,
19        UpsertOAuthAccount, User,
20    },
21};
22
23use crate::ports::{
24    ApiKeyRepository, AuditLogRepository, AuthorizationCodeRepository, CredentialRepository,
25    DeviceCodeRepository, InviteRepository, OAuthAccountRepository, OidcClientRepository,
26    OidcFederationProviderRepository, OidcTokenRepository, OrgRepository, SessionRepository,
27    UserRepository,
28};
29
30/// Acquire a read guard, recovering from a poisoned lock instead of panicking.
31macro_rules! rlock {
32    ($lock:expr, $label:literal) => {
33        match $lock.read() {
34            Ok(g) => g,
35            Err(e) => {
36                tracing::error!(concat!(
37                    "memory store read-lock poisoned (",
38                    $label,
39                    ") — recovering"
40                ));
41                e.into_inner()
42            }
43        }
44    };
45}
46
47/// Acquire a write guard, recovering from a poisoned lock instead of panicking.
48macro_rules! wlock {
49    ($lock:expr, $label:literal) => {
50        match $lock.write() {
51            Ok(g) => g,
52            Err(e) => {
53                tracing::error!(concat!(
54                    "memory store write-lock poisoned (",
55                    $label,
56                    ") — recovering"
57                ));
58                e.into_inner()
59            }
60        }
61    };
62}
63
64#[derive(Clone, Default)]
65pub struct MemoryStore {
66    users: Arc<RwLock<HashMap<Uuid, User>>>,
67    sessions: Arc<RwLock<HashMap<Uuid, Session>>>,
68    credentials: Arc<RwLock<Vec<Credential>>>,
69    audit_logs: Arc<RwLock<Vec<AuditLog>>>,
70    orgs: Arc<RwLock<HashMap<Uuid, Organization>>>,
71    roles: Arc<RwLock<HashMap<Uuid, Role>>>,
72    memberships: Arc<RwLock<Vec<Membership>>>,
73    api_keys: Arc<RwLock<Vec<ApiKey>>>,
74    oauth_accounts: Arc<RwLock<Vec<OAuthAccount>>>,
75    invites: Arc<RwLock<Vec<Invite>>>,
76    oidc_clients: Arc<RwLock<Vec<OidcClient>>>,
77    authorization_codes: Arc<RwLock<Vec<AuthorizationCode>>>,
78    oidc_tokens: Arc<RwLock<Vec<OidcToken>>>,
79    oidc_federation_providers: Arc<RwLock<Vec<OidcFederationProvider>>>,
80    device_codes: Arc<RwLock<Vec<DeviceCode>>>,
81}
82
83impl MemoryStore {
84    pub fn new() -> Self {
85        Self::default()
86    }
87}
88
89// ── UserRepository ────────────────────────────────────────────────────────────
90
91#[async_trait]
92impl UserRepository for MemoryStore {
93    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>> {
94        Ok(rlock!(self.users, "users").get(&id).cloned())
95    }
96
97    async fn find_by_email(&self, email: &str) -> Result<Option<User>> {
98        Ok(rlock!(self.users, "users")
99            .values()
100            .find(|u| u.email == email)
101            .cloned())
102    }
103
104    async fn find_by_username(&self, username: &str) -> Result<Option<User>> {
105        Ok(rlock!(self.users, "users")
106            .values()
107            .find(|u| u.username.as_deref() == Some(username))
108            .cloned())
109    }
110
111    async fn list(&self, offset: u32, limit: u32) -> Result<Vec<User>> {
112        let users = rlock!(self.users, "users");
113        let mut sorted: Vec<User> = users.values().cloned().collect();
114        sorted.sort_by_key(|u| u.created_at);
115        Ok(sorted
116            .into_iter()
117            .skip(offset as usize)
118            .take(limit as usize)
119            .collect())
120    }
121
122    async fn create(&self, data: CreateUser) -> Result<User> {
123        let mut users = wlock!(self.users, "users");
124        if users.values().any(|u| u.email == data.email) {
125            return Err(AuthError::EmailTaken);
126        }
127        if let Some(ref uname) = data.username
128            && users
129                .values()
130                .any(|u| u.username.as_deref() == Some(uname.as_str()))
131        {
132            return Err(AuthError::Storage(StorageError::Conflict(format!(
133                "username '{}' already taken",
134                uname
135            ))));
136        }
137        let user = User {
138            id: Uuid::new_v4(),
139            email: data.email,
140            email_verified: false,
141            username: data.username,
142            created_at: Utc::now(),
143            updated_at: Utc::now(),
144            metadata: data.metadata.unwrap_or(serde_json::Value::Null),
145        };
146        users.insert(user.id, user.clone());
147        Ok(user)
148    }
149
150    async fn update(&self, id: Uuid, data: UpdateUser) -> Result<User> {
151        let mut users = wlock!(self.users, "users");
152        let user = users.get_mut(&id).ok_or(AuthError::UserNotFound)?;
153        if let Some(email) = data.email {
154            user.email = email;
155        }
156        if let Some(verified) = data.email_verified {
157            user.email_verified = verified;
158        }
159        if let Some(uname) = data.username {
160            user.username = Some(uname);
161        }
162        if let Some(meta) = data.metadata {
163            user.metadata = meta;
164        }
165        user.updated_at = Utc::now();
166        Ok(user.clone())
167    }
168
169    async fn delete(&self, id: Uuid) -> Result<()> {
170        wlock!(self.users, "users")
171            .remove(&id)
172            .ok_or(AuthError::UserNotFound)?;
173        Ok(())
174    }
175}
176
177// ── SessionRepository ─────────────────────────────────────────────────────────
178
179#[async_trait]
180impl SessionRepository for MemoryStore {
181    async fn create(&self, data: CreateSession) -> Result<Session> {
182        let session = Session {
183            id: Uuid::new_v4(),
184            user_id: data.user_id,
185            token_hash: data.token_hash,
186            device_info: data.device_info,
187            ip_address: data.ip_address,
188            org_id: data.org_id,
189            expires_at: data.expires_at,
190            created_at: Utc::now(),
191        };
192        wlock!(self.sessions, "sessions").insert(session.id, session.clone());
193        Ok(session)
194    }
195
196    async fn find_by_token_hash(&self, hash: &str) -> Result<Option<Session>> {
197        Ok(rlock!(self.sessions, "sessions")
198            .values()
199            .find(|s| s.token_hash == hash && s.expires_at > Utc::now())
200            .cloned())
201    }
202
203    async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<Session>> {
204        Ok(rlock!(self.sessions, "sessions")
205            .values()
206            .filter(|s| s.user_id == user_id)
207            .cloned()
208            .collect())
209    }
210
211    async fn invalidate(&self, session_id: Uuid) -> Result<()> {
212        wlock!(self.sessions, "sessions")
213            .remove(&session_id)
214            .ok_or(AuthError::Storage(StorageError::NotFound))?;
215        Ok(())
216    }
217
218    async fn invalidate_all_for_user(&self, user_id: Uuid) -> Result<()> {
219        wlock!(self.sessions, "sessions").retain(|_, s| s.user_id != user_id);
220        Ok(())
221    }
222
223    async fn set_org(&self, session_id: Uuid, org_id: Option<Uuid>) -> Result<Session> {
224        let mut sessions = wlock!(self.sessions, "sessions");
225        let session = sessions
226            .get_mut(&session_id)
227            .ok_or(AuthError::Storage(StorageError::NotFound))?;
228        session.org_id = org_id;
229        Ok(session.clone())
230    }
231}
232
233// ── CredentialRepository ──────────────────────────────────────────────────────
234
235#[async_trait]
236impl CredentialRepository for MemoryStore {
237    async fn create(&self, data: CreateCredential) -> Result<Credential> {
238        let cred = Credential {
239            id: Uuid::new_v4(),
240            user_id: data.user_id,
241            kind: data.kind,
242            credential_hash: data.credential_hash,
243            metadata: data.metadata.unwrap_or(serde_json::Value::Null),
244        };
245        wlock!(self.credentials, "credentials").push(cred.clone());
246        Ok(cred)
247    }
248
249    async fn find_password_hash(&self, user_id: Uuid) -> Result<Option<String>> {
250        Ok(rlock!(self.credentials, "credentials")
251            .iter()
252            .find(|c| c.user_id == user_id && c.kind == CredentialKind::Password)
253            .map(|c| c.credential_hash.clone()))
254    }
255
256    async fn find_by_user_and_kind(
257        &self,
258        user_id: Uuid,
259        kind: CredentialKind,
260    ) -> Result<Option<Credential>> {
261        Ok(rlock!(self.credentials, "credentials")
262            .iter()
263            .find(|c| c.user_id == user_id && c.kind == kind)
264            .cloned())
265    }
266
267    async fn delete_by_user_and_kind(&self, user_id: Uuid, kind: CredentialKind) -> Result<()> {
268        let mut creds = wlock!(self.credentials, "credentials");
269        let before = creds.len();
270        creds.retain(|c| !(c.user_id == user_id && c.kind == kind));
271        if creds.len() == before {
272            return Err(AuthError::Storage(StorageError::NotFound));
273        }
274        Ok(())
275    }
276}
277
278// ── OrgRepository ─────────────────────────────────────────────────────────────
279
280#[async_trait]
281impl OrgRepository for MemoryStore {
282    async fn create(&self, data: CreateOrg) -> Result<Organization> {
283        let mut orgs = wlock!(self.orgs, "orgs");
284        if orgs.values().any(|o| o.slug == data.slug) {
285            return Err(AuthError::Storage(StorageError::Conflict(format!(
286                "slug '{}' already taken",
287                data.slug
288            ))));
289        }
290        let org = Organization {
291            id: Uuid::new_v4(),
292            name: data.name,
293            slug: data.slug,
294            metadata: data.metadata.unwrap_or(serde_json::Value::Null),
295            created_at: Utc::now(),
296        };
297        orgs.insert(org.id, org.clone());
298        Ok(org)
299    }
300
301    async fn find_by_id(&self, id: Uuid) -> Result<Option<Organization>> {
302        Ok(rlock!(self.orgs, "orgs").get(&id).cloned())
303    }
304
305    async fn find_by_slug(&self, slug: &str) -> Result<Option<Organization>> {
306        Ok(rlock!(self.orgs, "orgs")
307            .values()
308            .find(|o| o.slug == slug)
309            .cloned())
310    }
311
312    async fn add_member(&self, org_id: Uuid, user_id: Uuid, role_id: Uuid) -> Result<Membership> {
313        let role = rlock!(self.roles, "roles")
314            .get(&role_id)
315            .cloned()
316            .ok_or(AuthError::Storage(StorageError::NotFound))?;
317        let membership = Membership {
318            id: Uuid::new_v4(),
319            user_id,
320            org_id,
321            role,
322            created_at: Utc::now(),
323        };
324        wlock!(self.memberships, "memberships").push(membership.clone());
325        Ok(membership)
326    }
327
328    async fn remove_member(&self, org_id: Uuid, user_id: Uuid) -> Result<()> {
329        let mut memberships = wlock!(self.memberships, "memberships");
330        let before = memberships.len();
331        memberships.retain(|m| !(m.org_id == org_id && m.user_id == user_id));
332        if memberships.len() == before {
333            return Err(AuthError::Storage(StorageError::NotFound));
334        }
335        Ok(())
336    }
337
338    async fn get_members(&self, org_id: Uuid) -> Result<Vec<Membership>> {
339        Ok(rlock!(self.memberships, "memberships")
340            .iter()
341            .filter(|m| m.org_id == org_id)
342            .cloned()
343            .collect())
344    }
345
346    async fn find_roles(&self, org_id: Uuid) -> Result<Vec<Role>> {
347        Ok(rlock!(self.roles, "roles")
348            .values()
349            .filter(|r| r.org_id == org_id)
350            .cloned()
351            .collect())
352    }
353
354    async fn create_role(
355        &self,
356        org_id: Uuid,
357        name: String,
358        permissions: Vec<String>,
359    ) -> Result<Role> {
360        let role = Role {
361            id: Uuid::new_v4(),
362            org_id,
363            name,
364            permissions,
365        };
366        wlock!(self.roles, "roles").insert(role.id, role.clone());
367        Ok(role)
368    }
369
370    async fn update_member_role(
371        &self,
372        org_id: Uuid,
373        user_id: Uuid,
374        role_id: Uuid,
375    ) -> Result<Membership> {
376        let role = rlock!(self.roles, "roles")
377            .get(&role_id)
378            .cloned()
379            .ok_or(AuthError::Storage(StorageError::NotFound))?;
380
381        let mut memberships = wlock!(self.memberships, "memberships");
382        let m = memberships
383            .iter_mut()
384            .find(|m| m.org_id == org_id && m.user_id == user_id)
385            .ok_or(AuthError::Storage(StorageError::NotFound))?;
386        m.role = role;
387        Ok(m.clone())
388    }
389}
390
391// ── AuditLogRepository ────────────────────────────────────────────────────────
392
393#[async_trait]
394impl AuditLogRepository for MemoryStore {
395    async fn append(&self, entry: CreateAuditLog) -> Result<AuditLog> {
396        let log = AuditLog {
397            id: Uuid::new_v4(),
398            user_id: entry.user_id,
399            org_id: entry.org_id,
400            action: entry.action,
401            resource_type: entry.resource_type,
402            resource_id: entry.resource_id,
403            ip_address: entry.ip_address,
404            metadata: entry.metadata.unwrap_or(serde_json::Value::Null),
405            created_at: Utc::now(),
406        };
407        wlock!(self.audit_logs, "audit_logs").push(log.clone());
408        Ok(log)
409    }
410
411    async fn find_by_user(&self, user_id: Uuid, limit: u32) -> Result<Vec<AuditLog>> {
412        Ok(rlock!(self.audit_logs, "audit_logs")
413            .iter()
414            .filter(|l| l.user_id == Some(user_id))
415            .take(limit as usize)
416            .cloned()
417            .collect())
418    }
419
420    async fn find_by_org(&self, org_id: Uuid, limit: u32) -> Result<Vec<AuditLog>> {
421        Ok(rlock!(self.audit_logs, "audit_logs")
422            .iter()
423            .filter(|l| l.org_id == Some(org_id))
424            .take(limit as usize)
425            .cloned()
426            .collect())
427    }
428}
429
430// ── ApiKeyRepository ──────────────────────────────────────────────────────────
431
432#[async_trait]
433impl ApiKeyRepository for MemoryStore {
434    async fn create(&self, data: CreateApiKey) -> Result<ApiKey> {
435        let key = ApiKey {
436            id: Uuid::new_v4(),
437            user_id: data.user_id,
438            org_id: data.org_id,
439            key_hash: data.key_hash,
440            prefix: data.prefix,
441            name: data.name,
442            scopes: data.scopes,
443            expires_at: data.expires_at,
444            last_used_at: None,
445        };
446        wlock!(self.api_keys, "api_keys").push(key.clone());
447        Ok(key)
448    }
449
450    async fn find_by_hash(&self, key_hash: &str) -> Result<Option<ApiKey>> {
451        Ok(rlock!(self.api_keys, "api_keys")
452            .iter()
453            .find(|k| k.key_hash == key_hash)
454            .cloned())
455    }
456
457    async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ApiKey>> {
458        Ok(rlock!(self.api_keys, "api_keys")
459            .iter()
460            .filter(|k| k.user_id == user_id)
461            .cloned()
462            .collect())
463    }
464
465    async fn revoke(&self, key_id: Uuid, user_id: Uuid) -> Result<()> {
466        let mut keys = wlock!(self.api_keys, "api_keys");
467        let before = keys.len();
468        keys.retain(|k| !(k.id == key_id && k.user_id == user_id));
469        if keys.len() == before {
470            return Err(AuthError::Storage(StorageError::NotFound));
471        }
472        Ok(())
473    }
474
475    async fn touch_last_used(&self, key_id: Uuid, at: chrono::DateTime<Utc>) -> Result<()> {
476        let mut keys = wlock!(self.api_keys, "api_keys");
477        if let Some(k) = keys.iter_mut().find(|k| k.id == key_id) {
478            k.last_used_at = Some(at);
479        }
480        Ok(())
481    }
482}
483
484// ── OAuthAccountRepository ────────────────────────────────────────────────────
485
486#[async_trait]
487impl OAuthAccountRepository for MemoryStore {
488    async fn upsert(&self, data: UpsertOAuthAccount) -> Result<OAuthAccount> {
489        let mut accounts = wlock!(self.oauth_accounts, "oauth_accounts");
490        if let Some(existing) = accounts
491            .iter_mut()
492            .find(|a| a.provider == data.provider && a.provider_user_id == data.provider_user_id)
493        {
494            existing.access_token_enc = data.access_token_enc;
495            existing.refresh_token_enc = data.refresh_token_enc;
496            existing.expires_at = data.expires_at;
497            return Ok(existing.clone());
498        }
499        let account = OAuthAccount {
500            id: Uuid::new_v4(),
501            user_id: data.user_id,
502            provider: data.provider,
503            provider_user_id: data.provider_user_id,
504            access_token_enc: data.access_token_enc,
505            refresh_token_enc: data.refresh_token_enc,
506            expires_at: data.expires_at,
507        };
508        accounts.push(account.clone());
509        Ok(account)
510    }
511
512    async fn find_by_provider(
513        &self,
514        provider: &str,
515        provider_user_id: &str,
516    ) -> Result<Option<OAuthAccount>> {
517        Ok(rlock!(self.oauth_accounts, "oauth_accounts")
518            .iter()
519            .find(|a| a.provider == provider && a.provider_user_id == provider_user_id)
520            .cloned())
521    }
522
523    async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<OAuthAccount>> {
524        Ok(rlock!(self.oauth_accounts, "oauth_accounts")
525            .iter()
526            .filter(|a| a.user_id == user_id)
527            .cloned()
528            .collect())
529    }
530
531    async fn delete(&self, id: Uuid) -> Result<()> {
532        let mut accounts = wlock!(self.oauth_accounts, "oauth_accounts");
533        let before = accounts.len();
534        accounts.retain(|a| a.id != id);
535        if accounts.len() == before {
536            return Err(AuthError::Storage(StorageError::NotFound));
537        }
538        Ok(())
539    }
540}
541
542// ── InviteRepository ──────────────────────────────────────────────────────────
543
544#[async_trait]
545impl InviteRepository for MemoryStore {
546    async fn create(&self, data: CreateInvite) -> Result<Invite> {
547        let invite = Invite {
548            id: Uuid::new_v4(),
549            org_id: data.org_id,
550            email: data.email,
551            role_id: data.role_id,
552            token_hash: data.token_hash,
553            expires_at: data.expires_at,
554            accepted_at: None,
555        };
556        wlock!(self.invites, "invites").push(invite.clone());
557        Ok(invite)
558    }
559
560    async fn find_by_token_hash(&self, hash: &str) -> Result<Option<Invite>> {
561        Ok(rlock!(self.invites, "invites")
562            .iter()
563            .find(|i| i.token_hash == hash)
564            .cloned())
565    }
566
567    async fn accept(&self, invite_id: Uuid) -> Result<Invite> {
568        let mut invites = wlock!(self.invites, "invites");
569        let invite = invites
570            .iter_mut()
571            .find(|i| i.id == invite_id)
572            .ok_or(AuthError::Storage(StorageError::NotFound))?;
573        invite.accepted_at = Some(Utc::now());
574        Ok(invite.clone())
575    }
576
577    async fn delete_expired(&self) -> Result<u64> {
578        let mut invites = wlock!(self.invites, "invites");
579        let before = invites.len();
580        let now = Utc::now();
581        invites.retain(|i| i.accepted_at.is_some() || i.expires_at > now);
582        Ok((before - invites.len()) as u64)
583    }
584}
585
586// ── OidcClientRepository ───────────────────────────────────────────────────────
587
588#[async_trait]
589impl OidcClientRepository for MemoryStore {
590    async fn create(&self, data: CreateOidcClient) -> Result<OidcClient> {
591        let client_id = Uuid::new_v4().to_string();
592        let client = OidcClient {
593            id: Uuid::new_v4(),
594            client_id: client_id.clone(),
595            secret_hash: data.secret_hash,
596            name: data.name,
597            redirect_uris: data.redirect_uris,
598            grant_types: data.grant_types,
599            response_types: data.response_types,
600            allowed_scopes: data.allowed_scopes,
601            created_at: Utc::now(),
602        };
603        wlock!(self.oidc_clients, "oidc_clients").push(client.clone());
604        Ok(client)
605    }
606
607    async fn find_by_client_id(&self, client_id: &str) -> Result<Option<OidcClient>> {
608        Ok(rlock!(self.oidc_clients, "oidc_clients")
609            .iter()
610            .find(|c| c.client_id == client_id)
611            .cloned())
612    }
613
614    async fn list(&self, offset: u32, limit: u32) -> Result<Vec<OidcClient>> {
615        let clients = rlock!(self.oidc_clients, "oidc_clients");
616        Ok(clients
617            .iter()
618            .skip(offset as usize)
619            .take(limit as usize)
620            .cloned()
621            .collect())
622    }
623}
624
625// ── AuthorizationCodeRepository ────────────────────────────────────────────────
626
627#[async_trait]
628impl AuthorizationCodeRepository for MemoryStore {
629    async fn create(&self, data: CreateAuthorizationCode) -> Result<AuthorizationCode> {
630        let code = AuthorizationCode {
631            id: Uuid::new_v4(),
632            code_hash: data.code_hash,
633            client_id: data.client_id,
634            user_id: data.user_id,
635            redirect_uri: data.redirect_uri,
636            scope: data.scope,
637            nonce: data.nonce,
638            code_challenge: data.code_challenge,
639            expires_at: data.expires_at,
640            used: false,
641        };
642        wlock!(self.authorization_codes, "authorization_codes").push(code.clone());
643        Ok(code)
644    }
645
646    async fn find_by_code_hash(&self, hash: &str) -> Result<Option<AuthorizationCode>> {
647        let now = Utc::now();
648        Ok(rlock!(self.authorization_codes, "authorization_codes")
649            .iter()
650            .find(|c| c.code_hash == hash && c.expires_at > now && !c.used)
651            .cloned())
652    }
653
654    async fn mark_used(&self, id: Uuid) -> Result<()> {
655        let mut codes = wlock!(self.authorization_codes, "authorization_codes");
656        let code = codes
657            .iter_mut()
658            .find(|c| c.id == id)
659            .ok_or(AuthError::Storage(StorageError::NotFound))?;
660        code.used = true;
661        Ok(())
662    }
663
664    async fn delete_expired(&self) -> Result<u64> {
665        let mut codes = wlock!(self.authorization_codes, "authorization_codes");
666        let before = codes.len();
667        let now = Utc::now();
668        codes.retain(|c| c.expires_at > now);
669        Ok((before - codes.len()) as u64)
670    }
671}
672
673// ── OidcTokenRepository ────────────────────────────────────────────────────────
674
675#[async_trait]
676impl OidcTokenRepository for MemoryStore {
677    async fn create(&self, data: CreateOidcToken) -> Result<OidcToken> {
678        let token = OidcToken {
679            id: Uuid::new_v4(),
680            token_hash: data.token_hash,
681            client_id: data.client_id,
682            user_id: data.user_id,
683            scope: data.scope,
684            token_type: data.token_type,
685            expires_at: data.expires_at,
686            revoked: false,
687            created_at: Utc::now(),
688        };
689        wlock!(self.oidc_tokens, "oidc_tokens").push(token.clone());
690        Ok(token)
691    }
692
693    async fn find_by_token_hash(&self, hash: &str) -> Result<Option<OidcToken>> {
694        let now = Utc::now();
695        Ok(rlock!(self.oidc_tokens, "oidc_tokens")
696            .iter()
697            .find(|t| {
698                t.token_hash == hash && !t.revoked && t.expires_at.map(|e| e > now).unwrap_or(true)
699            })
700            .cloned())
701    }
702
703    async fn revoke(&self, id: Uuid) -> Result<()> {
704        let mut tokens = wlock!(self.oidc_tokens, "oidc_tokens");
705        let t = tokens
706            .iter_mut()
707            .find(|t| t.id == id)
708            .ok_or(AuthError::Storage(StorageError::NotFound))?;
709        t.revoked = true;
710        Ok(())
711    }
712
713    async fn revoke_all_for_user_client(&self, user_id: Uuid, client_id: &str) -> Result<()> {
714        for t in wlock!(self.oidc_tokens, "oidc_tokens")
715            .iter_mut()
716            .filter(|t| t.user_id == user_id && t.client_id == client_id)
717        {
718            t.revoked = true;
719        }
720        Ok(())
721    }
722}
723
724// ── OidcFederationProviderRepository ──────────────────────────────────────────
725
726#[async_trait]
727impl OidcFederationProviderRepository for MemoryStore {
728    async fn create(&self, data: CreateOidcFederationProvider) -> Result<OidcFederationProvider> {
729        let provider = OidcFederationProvider {
730            id: Uuid::new_v4(),
731            name: data.name,
732            issuer: data.issuer,
733            client_id: data.client_id,
734            secret_enc: data.secret_enc,
735            scopes: data.scopes,
736            org_id: data.org_id,
737            enabled: true,
738            created_at: Utc::now(),
739            claim_mapping: data.claim_mapping,
740        };
741        wlock!(self.oidc_federation_providers, "oidc_federation_providers").push(provider.clone());
742        Ok(provider)
743    }
744
745    async fn find_by_id(&self, id: Uuid) -> Result<Option<OidcFederationProvider>> {
746        Ok(
747            rlock!(self.oidc_federation_providers, "oidc_federation_providers")
748                .iter()
749                .find(|p| p.id == id)
750                .cloned(),
751        )
752    }
753
754    async fn find_by_name(&self, name: &str) -> Result<Option<OidcFederationProvider>> {
755        Ok(
756            rlock!(self.oidc_federation_providers, "oidc_federation_providers")
757                .iter()
758                .find(|p| p.name == name)
759                .cloned(),
760        )
761    }
762
763    async fn list_enabled(&self) -> Result<Vec<OidcFederationProvider>> {
764        Ok(
765            rlock!(self.oidc_federation_providers, "oidc_federation_providers")
766                .iter()
767                .filter(|p| p.enabled)
768                .cloned()
769                .collect(),
770        )
771    }
772}
773
774// ── DeviceCodeRepository ─────────────────────────────────────────────────────
775
776#[async_trait]
777impl DeviceCodeRepository for MemoryStore {
778    async fn create(&self, data: CreateDeviceCode) -> Result<DeviceCode> {
779        let dc = DeviceCode {
780            id: Uuid::new_v4(),
781            device_code_hash: data.device_code_hash,
782            user_code_hash: data.user_code_hash,
783            user_code: data.user_code,
784            client_id: data.client_id,
785            scope: data.scope,
786            expires_at: data.expires_at,
787            interval_secs: data.interval_secs,
788            authorized: false,
789            denied: false,
790            user_id: None,
791            last_polled_at: None,
792        };
793        wlock!(self.device_codes, "device_codes").push(dc.clone());
794        Ok(dc)
795    }
796
797    async fn find_by_device_code_hash(&self, hash: &str) -> Result<Option<DeviceCode>> {
798        let now = Utc::now();
799        Ok(rlock!(self.device_codes, "device_codes")
800            .iter()
801            .find(|d| d.device_code_hash == hash && d.expires_at > now)
802            .cloned())
803    }
804
805    async fn find_by_user_code_hash(&self, hash: &str) -> Result<Option<DeviceCode>> {
806        let now = Utc::now();
807        Ok(rlock!(self.device_codes, "device_codes")
808            .iter()
809            .find(|d| d.user_code_hash == hash && d.expires_at > now && !d.authorized && !d.denied)
810            .cloned())
811    }
812
813    async fn authorize(&self, id: Uuid, user_id: Uuid) -> Result<()> {
814        let mut codes = wlock!(self.device_codes, "device_codes");
815        let dc = codes
816            .iter_mut()
817            .find(|d| d.id == id)
818            .ok_or(AuthError::Storage(StorageError::NotFound))?;
819        dc.authorized = true;
820        dc.user_id = Some(user_id);
821        Ok(())
822    }
823
824    async fn deny(&self, id: Uuid) -> Result<()> {
825        let mut codes = wlock!(self.device_codes, "device_codes");
826        let dc = codes
827            .iter_mut()
828            .find(|d| d.id == id)
829            .ok_or(AuthError::Storage(StorageError::NotFound))?;
830        dc.denied = true;
831        Ok(())
832    }
833
834    async fn update_last_polled(&self, id: Uuid, interval_secs: u32) -> Result<()> {
835        let mut codes = wlock!(self.device_codes, "device_codes");
836        if let Some(dc) = codes.iter_mut().find(|d| d.id == id) {
837            dc.last_polled_at = Some(Utc::now());
838            dc.interval_secs = interval_secs;
839        }
840        Ok(())
841    }
842
843    async fn delete(&self, id: Uuid) -> Result<()> {
844        let mut codes = wlock!(self.device_codes, "device_codes");
845        codes.retain(|d| d.id != id);
846        Ok(())
847    }
848
849    async fn delete_expired(&self) -> Result<u64> {
850        let mut codes = wlock!(self.device_codes, "device_codes");
851        let before = codes.len();
852        let now = Utc::now();
853        codes.retain(|d| d.expires_at > now);
854        Ok((before - codes.len()) as u64)
855    }
856
857    async fn list_by_client(
858        &self,
859        client_id: &str,
860        offset: u32,
861        limit: u32,
862    ) -> Result<Vec<DeviceCode>> {
863        Ok(rlock!(self.device_codes, "device_codes")
864            .iter()
865            .filter(|d| d.client_id == client_id)
866            .skip(offset as usize)
867            .take(limit as usize)
868            .cloned()
869            .collect())
870    }
871}