1use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7
8use crate::error::{CompatError, Result};
9use crate::rate_limit::RateLimiter;
10use crate::release::{AssetId, Release, ReleaseAsset, ReleaseId};
11use crate::ssh_key::{SshKey, SshKeyId};
12use crate::token::{PersonalAccessToken, TokenId, TokenScope, TokenValue};
13use crate::user::{User, UserId};
14
15#[derive(Debug, Clone)]
17pub struct CompatStore {
18 pub users: UserStore,
20 pub tokens: TokenStore,
22 pub ssh_keys: SshKeyStore,
24 pub releases: ReleaseStore,
26 pub rate_limiter: RateLimiter,
28}
29
30impl Default for CompatStore {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl CompatStore {
37 pub fn new() -> Self {
39 Self {
40 users: UserStore::new(),
41 tokens: TokenStore::new(),
42 ssh_keys: SshKeyStore::new(),
43 releases: ReleaseStore::new(),
44 rate_limiter: RateLimiter::new(),
45 }
46 }
47
48 pub fn stats(&self) -> CompatStats {
50 CompatStats {
51 users: self.users.count(),
52 tokens: self.tokens.count(),
53 ssh_keys: self.ssh_keys.count(),
54 releases: self.releases.count(),
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct UserStore {
62 users: Arc<RwLock<HashMap<UserId, User>>>,
64 username_index: Arc<RwLock<HashMap<String, UserId>>>,
66 pubkey_index: Arc<RwLock<HashMap<String, UserId>>>,
68 next_id: Arc<AtomicU64>,
70}
71
72impl Default for UserStore {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl UserStore {
79 pub fn new() -> Self {
81 Self {
82 users: Arc::new(RwLock::new(HashMap::new())),
83 username_index: Arc::new(RwLock::new(HashMap::new())),
84 pubkey_index: Arc::new(RwLock::new(HashMap::new())),
85 next_id: Arc::new(AtomicU64::new(1)),
86 }
87 }
88
89 pub fn create(&self, username: String, public_key: String) -> Result<User> {
91 User::validate_username(&username).map_err(CompatError::InvalidUsername)?;
93
94 let mut users = self.users.write();
95 let mut username_index = self.username_index.write();
96 let mut pubkey_index = self.pubkey_index.write();
97
98 if username_index.contains_key(&username) {
100 return Err(CompatError::UsernameExists(username));
101 }
102
103 if pubkey_index.contains_key(&public_key) {
105 return Err(CompatError::UsernameExists(
106 "public key already registered".to_string(),
107 ));
108 }
109
110 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
111 let user = User::new(id, username.clone(), public_key.clone());
112
113 username_index.insert(username, id);
114 pubkey_index.insert(public_key, id);
115 users.insert(id, user.clone());
116
117 Ok(user)
118 }
119
120 pub fn get(&self, id: UserId) -> Option<User> {
122 self.users.read().get(&id).cloned()
123 }
124
125 pub fn get_by_username(&self, username: &str) -> Option<User> {
127 let username_index = self.username_index.read();
128 let id = username_index.get(username)?;
129 self.users.read().get(id).cloned()
130 }
131
132 pub fn get_by_public_key(&self, public_key: &str) -> Option<User> {
134 let pubkey_index = self.pubkey_index.read();
135 let id = pubkey_index.get(public_key)?;
136 self.users.read().get(id).cloned()
137 }
138
139 pub fn update(&self, user: User) -> Result<User> {
141 let mut users = self.users.write();
142 if !users.contains_key(&user.id) {
143 return Err(CompatError::UserNotFound(user.id.to_string()));
144 }
145 users.insert(user.id, user.clone());
146 Ok(user)
147 }
148
149 pub fn list(&self) -> Vec<User> {
151 self.users.read().values().cloned().collect()
152 }
153
154 pub fn count(&self) -> usize {
156 self.users.read().len()
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct TokenStore {
163 tokens: Arc<RwLock<HashMap<TokenId, PersonalAccessToken>>>,
165 prefix_index: Arc<RwLock<HashMap<String, TokenId>>>,
167 next_id: Arc<AtomicU64>,
169}
170
171impl Default for TokenStore {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl TokenStore {
178 pub fn new() -> Self {
180 Self {
181 tokens: Arc::new(RwLock::new(HashMap::new())),
182 prefix_index: Arc::new(RwLock::new(HashMap::new())),
183 next_id: Arc::new(AtomicU64::new(1)),
184 }
185 }
186
187 pub fn create(
191 &self,
192 user_id: UserId,
193 name: String,
194 scopes: Vec<TokenScope>,
195 expires_at: Option<u64>,
196 ) -> Result<(PersonalAccessToken, String)> {
197 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
198 let (token, plaintext) =
199 PersonalAccessToken::generate(id, user_id, name, scopes, expires_at)?;
200
201 let mut tokens = self.tokens.write();
202 let mut prefix_index = self.prefix_index.write();
203
204 prefix_index.insert(token.token_prefix.clone(), id);
205 tokens.insert(id, token.clone());
206
207 Ok((token, plaintext))
208 }
209
210 pub fn get(&self, id: TokenId) -> Option<PersonalAccessToken> {
212 self.tokens.read().get(&id).cloned()
213 }
214
215 pub fn get_by_prefix(&self, prefix: &str) -> Option<PersonalAccessToken> {
217 let prefix_index = self.prefix_index.read();
218 let id = prefix_index.get(prefix)?;
219 self.tokens.read().get(id).cloned()
220 }
221
222 pub fn verify(&self, token_string: &str) -> Result<(UserId, Vec<TokenScope>)> {
224 let token_value = TokenValue::parse(token_string)?;
225
226 let prefix_index = self.prefix_index.read();
227 let id = prefix_index
228 .get(&token_value.prefix)
229 .ok_or(CompatError::TokenNotFound)?;
230
231 let mut tokens = self.tokens.write();
232 let token = tokens.get_mut(id).ok_or(CompatError::TokenNotFound)?;
233
234 token.verify(&token_value.secret)?;
236
237 if token.is_expired() {
239 return Err(CompatError::TokenExpired);
240 }
241
242 token.touch();
244
245 Ok((token.user_id, token.scopes.clone()))
246 }
247
248 pub fn revoke(&self, id: TokenId) -> Result<()> {
250 let mut tokens = self.tokens.write();
251 let mut prefix_index = self.prefix_index.write();
252
253 let token = tokens.remove(&id).ok_or(CompatError::TokenNotFound)?;
254 prefix_index.remove(&token.token_prefix);
255
256 Ok(())
257 }
258
259 pub fn list_for_user(&self, user_id: UserId) -> Vec<PersonalAccessToken> {
261 self.tokens
262 .read()
263 .values()
264 .filter(|t| t.user_id == user_id)
265 .cloned()
266 .collect()
267 }
268
269 pub fn count(&self) -> usize {
271 self.tokens.read().len()
272 }
273}
274
275#[derive(Debug, Clone)]
277pub struct SshKeyStore {
278 keys: Arc<RwLock<HashMap<SshKeyId, SshKey>>>,
280 fingerprint_index: Arc<RwLock<HashMap<String, SshKeyId>>>,
282 next_id: Arc<AtomicU64>,
284}
285
286impl Default for SshKeyStore {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292impl SshKeyStore {
293 pub fn new() -> Self {
295 Self {
296 keys: Arc::new(RwLock::new(HashMap::new())),
297 fingerprint_index: Arc::new(RwLock::new(HashMap::new())),
298 next_id: Arc::new(AtomicU64::new(1)),
299 }
300 }
301
302 pub fn add(&self, user_id: UserId, title: String, public_key: String) -> Result<SshKey> {
304 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
305 let key = SshKey::new(id, user_id, title, public_key)?;
306
307 let mut keys = self.keys.write();
308 let mut fingerprint_index = self.fingerprint_index.write();
309
310 if fingerprint_index.contains_key(&key.fingerprint) {
312 return Err(CompatError::SshKeyExists(key.fingerprint));
313 }
314
315 fingerprint_index.insert(key.fingerprint.clone(), id);
316 keys.insert(id, key.clone());
317
318 Ok(key)
319 }
320
321 pub fn get(&self, id: SshKeyId) -> Option<SshKey> {
323 self.keys.read().get(&id).cloned()
324 }
325
326 pub fn get_by_fingerprint(&self, fingerprint: &str) -> Option<SshKey> {
328 let fingerprint_index = self.fingerprint_index.read();
329 let id = fingerprint_index.get(fingerprint)?;
330 self.keys.read().get(id).cloned()
331 }
332
333 pub fn remove(&self, id: SshKeyId) -> Result<SshKey> {
335 let mut keys = self.keys.write();
336 let mut fingerprint_index = self.fingerprint_index.write();
337
338 let key = keys.remove(&id).ok_or(CompatError::SshKeyNotFound)?;
339 fingerprint_index.remove(&key.fingerprint);
340
341 Ok(key)
342 }
343
344 pub fn list_for_user(&self, user_id: UserId) -> Vec<SshKey> {
346 self.keys
347 .read()
348 .values()
349 .filter(|k| k.user_id == user_id)
350 .cloned()
351 .collect()
352 }
353
354 pub fn count(&self) -> usize {
356 self.keys.read().len()
357 }
358}
359
360#[derive(Debug, Clone)]
362pub struct ReleaseStore {
363 releases: Arc<RwLock<HashMap<ReleaseId, Release>>>,
365 tag_index: Arc<RwLock<HashMap<(String, String), ReleaseId>>>,
367 asset_content: Arc<RwLock<HashMap<String, Vec<u8>>>>,
369 next_release_id: Arc<AtomicU64>,
371 next_asset_id: Arc<AtomicU64>,
373}
374
375impl Default for ReleaseStore {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381impl ReleaseStore {
382 pub fn new() -> Self {
384 Self {
385 releases: Arc::new(RwLock::new(HashMap::new())),
386 tag_index: Arc::new(RwLock::new(HashMap::new())),
387 asset_content: Arc::new(RwLock::new(HashMap::new())),
388 next_release_id: Arc::new(AtomicU64::new(1)),
389 next_asset_id: Arc::new(AtomicU64::new(1)),
390 }
391 }
392
393 pub fn create(
395 &self,
396 repo_key: String,
397 tag_name: String,
398 target_commitish: String,
399 author: String,
400 ) -> Result<Release> {
401 let mut releases = self.releases.write();
402 let mut tag_index = self.tag_index.write();
403
404 let key = (repo_key.clone(), tag_name.clone());
406 if tag_index.contains_key(&key) {
407 return Err(CompatError::ReleaseExists(tag_name));
408 }
409
410 let id = self.next_release_id.fetch_add(1, Ordering::SeqCst);
411 let release = Release::new(id, repo_key, tag_name, target_commitish, author);
412
413 tag_index.insert(key, id);
414 releases.insert(id, release.clone());
415
416 Ok(release)
417 }
418
419 pub fn get(&self, id: ReleaseId) -> Option<Release> {
421 self.releases.read().get(&id).cloned()
422 }
423
424 pub fn get_by_tag(&self, repo_key: &str, tag_name: &str) -> Option<Release> {
426 let tag_index = self.tag_index.read();
427 let id = tag_index.get(&(repo_key.to_string(), tag_name.to_string()))?;
428 self.releases.read().get(id).cloned()
429 }
430
431 pub fn get_latest(&self, repo_key: &str) -> Option<Release> {
433 self.releases
434 .read()
435 .values()
436 .filter(|r| r.repo_key == repo_key && r.is_publishable())
437 .max_by_key(|r| r.published_at)
438 .cloned()
439 }
440
441 pub fn update(&self, release: Release) -> Result<Release> {
443 let mut releases = self.releases.write();
444 if !releases.contains_key(&release.id) {
445 return Err(CompatError::ReleaseNotFound(release.id.to_string()));
446 }
447 releases.insert(release.id, release.clone());
448 Ok(release)
449 }
450
451 pub fn delete(&self, id: ReleaseId) -> Result<Release> {
453 let mut releases = self.releases.write();
454 let mut tag_index = self.tag_index.write();
455
456 let release = releases
457 .remove(&id)
458 .ok_or_else(|| CompatError::ReleaseNotFound(id.to_string()))?;
459 tag_index.remove(&(release.repo_key.clone(), release.tag_name.clone()));
460
461 Ok(release)
462 }
463
464 pub fn list(&self, repo_key: &str) -> Vec<Release> {
466 let mut releases: Vec<_> = self
467 .releases
468 .read()
469 .values()
470 .filter(|r| r.repo_key == repo_key)
471 .cloned()
472 .collect();
473 releases.sort_by(|a, b| b.created_at.cmp(&a.created_at));
474 releases
475 }
476
477 pub fn add_asset(
479 &self,
480 release_id: ReleaseId,
481 name: String,
482 content_type: String,
483 content: Vec<u8>,
484 uploader: String,
485 ) -> Result<ReleaseAsset> {
486 let mut releases = self.releases.write();
487 let mut asset_content = self.asset_content.write();
488
489 let release = releases
490 .get_mut(&release_id)
491 .ok_or_else(|| CompatError::ReleaseNotFound(release_id.to_string()))?;
492
493 if release.assets.iter().any(|a| a.name == name) {
495 return Err(CompatError::AssetExists(name));
496 }
497
498 use sha2::{Digest, Sha256};
500 let hash = hex::encode(Sha256::digest(&content));
501
502 let id = self.next_asset_id.fetch_add(1, Ordering::SeqCst);
503 let asset = ReleaseAsset::new(
504 id,
505 release_id,
506 name,
507 content_type,
508 content.len() as u64,
509 hash.clone(),
510 uploader,
511 );
512
513 asset_content.insert(hash, content);
515
516 release.add_asset(asset.clone());
518
519 Ok(asset)
520 }
521
522 pub fn get_asset_content(&self, content_hash: &str) -> Option<Vec<u8>> {
524 self.asset_content.read().get(content_hash).cloned()
525 }
526
527 pub fn delete_asset(&self, release_id: ReleaseId, asset_id: AssetId) -> Result<ReleaseAsset> {
529 let mut releases = self.releases.write();
530 let mut asset_content = self.asset_content.write();
531
532 let release = releases
533 .get_mut(&release_id)
534 .ok_or_else(|| CompatError::ReleaseNotFound(release_id.to_string()))?;
535
536 let asset = release
537 .remove_asset(asset_id)
538 .ok_or_else(|| CompatError::AssetNotFound(asset_id.to_string()))?;
539
540 asset_content.remove(&asset.content_hash);
542
543 Ok(asset)
544 }
545
546 pub fn count(&self) -> usize {
548 self.releases.read().len()
549 }
550}
551
552#[derive(Debug, Clone, Serialize)]
554pub struct CompatStats {
555 pub users: usize,
557 pub tokens: usize,
559 pub ssh_keys: usize,
561 pub releases: usize,
563}
564
565use serde::Serialize;
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_user_store() {
573 let store = UserStore::new();
574
575 let user = store
577 .create("alice".to_string(), "pubkey123".to_string())
578 .unwrap();
579 assert_eq!(user.username, "alice");
580
581 let found = store.get(user.id).unwrap();
583 assert_eq!(found.username, "alice");
584
585 let found = store.get_by_username("alice").unwrap();
587 assert_eq!(found.id, user.id);
588
589 let result = store.create("alice".to_string(), "pubkey456".to_string());
591 assert!(result.is_err());
592 }
593
594 #[test]
595 fn test_token_store() {
596 let store = TokenStore::new();
597
598 let (token, plaintext) = store
600 .create(
601 1,
602 "test token".to_string(),
603 vec![TokenScope::RepoRead],
604 None,
605 )
606 .unwrap();
607
608 assert!(plaintext.starts_with("guts_"));
609
610 let (user_id, scopes) = store.verify(&plaintext).unwrap();
612 assert_eq!(user_id, 1);
613 assert!(scopes.contains(&TokenScope::RepoRead));
614
615 store.revoke(token.id).unwrap();
617
618 let result = store.verify(&plaintext);
620 assert!(result.is_err());
621 }
622
623 #[test]
624 fn test_ssh_key_store() {
625 let store = SshKeyStore::new();
626
627 let key = store
629 .add(
630 1,
631 "My Key".to_string(),
632 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl test@example.com".to_string(),
633 )
634 .unwrap();
635
636 let found = store.get(key.id).unwrap();
638 assert_eq!(found.title, "My Key");
639
640 let keys = store.list_for_user(1);
642 assert_eq!(keys.len(), 1);
643
644 store.remove(key.id).unwrap();
646 assert!(store.get(key.id).is_none());
647 }
648
649 #[test]
650 fn test_release_store() {
651 let store = ReleaseStore::new();
652
653 let release = store
655 .create(
656 "alice/repo".to_string(),
657 "v1.0.0".to_string(),
658 "main".to_string(),
659 "alice".to_string(),
660 )
661 .unwrap();
662
663 assert_eq!(release.tag_name, "v1.0.0");
664
665 let found = store.get(release.id).unwrap();
667 assert_eq!(found.tag_name, "v1.0.0");
668
669 let found = store.get_by_tag("alice/repo", "v1.0.0").unwrap();
671 assert_eq!(found.id, release.id);
672
673 let latest = store.get_latest("alice/repo").unwrap();
675 assert_eq!(latest.id, release.id);
676
677 let asset = store
679 .add_asset(
680 release.id,
681 "app.tar.gz".to_string(),
682 "application/gzip".to_string(),
683 b"test content".to_vec(),
684 "alice".to_string(),
685 )
686 .unwrap();
687
688 assert_eq!(asset.name, "app.tar.gz");
689
690 let content = store.get_asset_content(&asset.content_hash).unwrap();
692 assert_eq!(content, b"test content");
693
694 store.delete(release.id).unwrap();
696 assert!(store.get(release.id).is_none());
697 }
698}