1pub mod api_key;
37pub mod rate_limit;
38pub mod types;
39
40use chrono::Utc;
41use sqlx::{Row, SqlitePool};
42use tracing::{debug, info};
43use uuid::Uuid;
44
45use starpod_core::{Result, StarpodError};
46
47pub use rate_limit::RateLimiter;
48pub use types::*;
49
50pub struct AuthStore {
57 pool: SqlitePool,
58}
59
60impl AuthStore {
61 pub fn from_pool(pool: SqlitePool) -> Self {
65 Self { pool }
66 }
67
68 pub async fn create_user(
75 &self,
76 email: Option<&str>,
77 display_name: Option<&str>,
78 role: Role,
79 ) -> Result<User> {
80 let id = Uuid::new_v4().to_string();
81 let now = Utc::now();
82 let now_str = now.to_rfc3339();
83 let role_str = role.as_str();
84
85 sqlx::query(
86 "INSERT INTO users (id, email, display_name, role, is_active, created_at, updated_at) \
87 VALUES (?, ?, ?, ?, 1, ?, ?)",
88 )
89 .bind(&id)
90 .bind(email)
91 .bind(display_name)
92 .bind(role_str)
93 .bind(&now_str)
94 .bind(&now_str)
95 .execute(&self.pool)
96 .await
97 .map_err(|e| StarpodError::Auth(format!("Failed to create user: {}", e)))?;
98
99 debug!(user_id = %id, role = %role_str, "User created");
100
101 Ok(User {
102 id,
103 email: email.map(String::from),
104 display_name: display_name.map(String::from),
105 role,
106 is_active: true,
107 filesystem_enabled: false,
108 created_at: now,
109 updated_at: now,
110 })
111 }
112
113 pub async fn get_user(&self, id: &str) -> Result<Option<User>> {
115 let row = sqlx::query(
116 "SELECT id, email, display_name, role, is_active, filesystem_enabled, created_at, updated_at FROM users WHERE id = ?"
117 )
118 .bind(id)
119 .fetch_optional(&self.pool)
120 .await
121 .map_err(|e| StarpodError::Auth(format!("Failed to get user: {}", e)))?;
122
123 Ok(row.map(|r| row_to_user(&r)))
124 }
125
126 pub async fn list_users(&self) -> Result<Vec<User>> {
128 let rows = sqlx::query(
129 "SELECT id, email, display_name, role, is_active, filesystem_enabled, created_at, updated_at \
130 FROM users ORDER BY created_at ASC"
131 )
132 .fetch_all(&self.pool)
133 .await
134 .map_err(|e| StarpodError::Auth(format!("Failed to list users: {}", e)))?;
135
136 Ok(rows.iter().map(row_to_user).collect())
137 }
138
139 pub async fn update_user(
144 &self,
145 id: &str,
146 email: Option<&str>,
147 display_name: Option<&str>,
148 role: Option<Role>,
149 filesystem_enabled: Option<bool>,
150 ) -> Result<()> {
151 let now = Utc::now().to_rfc3339();
152
153 let role_clause = role
154 .map(|r| format!(", role = '{}'", r.as_str()))
155 .unwrap_or_default();
156 let fs_clause = filesystem_enabled
157 .map(|v| format!(", filesystem_enabled = {}", v as i32))
158 .unwrap_or_default();
159
160 let sql = format!(
161 "UPDATE users SET email = COALESCE(?, email), display_name = COALESCE(?, display_name){}{}, \
162 updated_at = ? WHERE id = ?",
163 role_clause, fs_clause,
164 );
165
166 sqlx::query(&sql)
167 .bind(email)
168 .bind(display_name)
169 .bind(&now)
170 .bind(id)
171 .execute(&self.pool)
172 .await
173 .map_err(|e| StarpodError::Auth(format!("Failed to update user: {}", e)))?;
174
175 Ok(())
176 }
177
178 pub async fn deactivate_user(&self, id: &str) -> Result<()> {
183 let now = Utc::now().to_rfc3339();
184 sqlx::query("UPDATE users SET is_active = 0, updated_at = ? WHERE id = ?")
185 .bind(&now)
186 .bind(id)
187 .execute(&self.pool)
188 .await
189 .map_err(|e| StarpodError::Auth(format!("Failed to deactivate user: {}", e)))?;
190 Ok(())
191 }
192
193 pub async fn activate_user(&self, id: &str) -> Result<()> {
195 let now = Utc::now().to_rfc3339();
196 sqlx::query("UPDATE users SET is_active = 1, updated_at = ? WHERE id = ?")
197 .bind(&now)
198 .bind(id)
199 .execute(&self.pool)
200 .await
201 .map_err(|e| StarpodError::Auth(format!("Failed to activate user: {}", e)))?;
202 Ok(())
203 }
204
205 pub async fn create_api_key(
209 &self,
210 user_id: &str,
211 label: Option<&str>,
212 ) -> Result<ApiKeyCreated> {
213 let key = api_key::generate_key();
214 let prefix = api_key::extract_prefix(&key)
215 .ok_or_else(|| StarpodError::Auth("Failed to extract key prefix".into()))?
216 .to_string();
217 let hash = api_key::hash_key(&key)
218 .map_err(|e| StarpodError::Auth(format!("Failed to hash key: {}", e)))?;
219
220 let id = Uuid::new_v4().to_string();
221 let now = Utc::now();
222 let now_str = now.to_rfc3339();
223
224 sqlx::query(
225 "INSERT INTO api_keys (id, user_id, prefix, key_hash, label, created_at) \
226 VALUES (?, ?, ?, ?, ?, ?)",
227 )
228 .bind(&id)
229 .bind(user_id)
230 .bind(&prefix)
231 .bind(&hash)
232 .bind(label)
233 .bind(&now_str)
234 .execute(&self.pool)
235 .await
236 .map_err(|e| StarpodError::Auth(format!("Failed to create API key: {}", e)))?;
237
238 debug!(user_id = %user_id, prefix = %prefix, "API key created");
239
240 Ok(ApiKeyCreated {
241 meta: ApiKeyMeta {
242 id,
243 user_id: user_id.to_string(),
244 prefix,
245 label: label.map(String::from),
246 expires_at: None,
247 revoked_at: None,
248 last_used_at: None,
249 created_at: now,
250 },
251 key,
252 })
253 }
254
255 pub async fn import_api_key(
257 &self,
258 user_id: &str,
259 plaintext_key: &str,
260 label: Option<&str>,
261 ) -> Result<ApiKeyMeta> {
262 let prefix = api_key::extract_prefix(plaintext_key)
263 .unwrap_or_else(|| &plaintext_key[..plaintext_key.len().min(8)])
264 .to_string();
265
266 let hash = api_key::hash_key(plaintext_key)
267 .map_err(|e| StarpodError::Auth(format!("Failed to hash key: {}", e)))?;
268
269 let id = Uuid::new_v4().to_string();
270 let now = Utc::now();
271 let now_str = now.to_rfc3339();
272
273 sqlx::query(
274 "INSERT INTO api_keys (id, user_id, prefix, key_hash, label, created_at) \
275 VALUES (?, ?, ?, ?, ?, ?)",
276 )
277 .bind(&id)
278 .bind(user_id)
279 .bind(&prefix)
280 .bind(&hash)
281 .bind(label)
282 .bind(&now_str)
283 .execute(&self.pool)
284 .await
285 .map_err(|e| StarpodError::Auth(format!("Failed to import API key: {}", e)))?;
286
287 info!(user_id = %user_id, prefix = %prefix, "API key imported");
288
289 Ok(ApiKeyMeta {
290 id,
291 user_id: user_id.to_string(),
292 prefix,
293 label: label.map(String::from),
294 expires_at: None,
295 revoked_at: None,
296 last_used_at: None,
297 created_at: now,
298 })
299 }
300
301 pub async fn authenticate_api_key(&self, key: &str) -> Result<Option<User>> {
303 let prefix = api_key::extract_prefix(key).unwrap_or_else(|| &key[..key.len().min(8)]);
305
306 let candidates = sqlx::query(
307 "SELECT ak.id AS ak_id, ak.key_hash, u.id, u.email, u.display_name, u.role, u.is_active, \
308 u.filesystem_enabled, u.created_at, u.updated_at \
309 FROM api_keys ak JOIN users u ON ak.user_id = u.id \
310 WHERE ak.prefix = ? AND ak.revoked_at IS NULL AND u.is_active = 1"
311 )
312 .bind(prefix)
313 .fetch_all(&self.pool)
314 .await
315 .map_err(|e| StarpodError::Auth(format!("Auth query failed: {}", e)))?;
316
317 for row in &candidates {
318 let hash: String = row.get("key_hash");
319 if api_key::verify_key(key, &hash) {
320 let ak_id: String = row.get("ak_id");
321 let now = Utc::now().to_rfc3339();
323 let _ = sqlx::query("UPDATE api_keys SET last_used_at = ? WHERE id = ?")
324 .bind(&now)
325 .bind(&ak_id)
326 .execute(&self.pool)
327 .await;
328
329 return Ok(Some(User {
330 id: row.get("id"),
331 email: row.get("email"),
332 display_name: row.get("display_name"),
333 role: Role::from_str(row.get::<&str, _>("role")).unwrap_or(Role::User),
334 is_active: row.get::<bool, _>("is_active"),
335 filesystem_enabled: row.get::<bool, _>("filesystem_enabled"),
336 created_at: parse_dt(row.get("created_at")),
337 updated_at: parse_dt(row.get("updated_at")),
338 }));
339 }
340 }
341
342 Ok(None)
343 }
344
345 pub async fn list_api_keys(&self, user_id: &str) -> Result<Vec<ApiKeyMeta>> {
347 let rows = sqlx::query(
348 "SELECT id, user_id, prefix, label, expires_at, revoked_at, last_used_at, created_at \
349 FROM api_keys WHERE user_id = ? ORDER BY created_at DESC",
350 )
351 .bind(user_id)
352 .fetch_all(&self.pool)
353 .await
354 .map_err(|e| StarpodError::Auth(format!("Failed to list API keys: {}", e)))?;
355
356 Ok(rows.iter().map(row_to_api_key_meta).collect())
357 }
358
359 pub async fn revoke_api_key(&self, key_id: &str) -> Result<()> {
364 let now = Utc::now().to_rfc3339();
365 sqlx::query("UPDATE api_keys SET revoked_at = ? WHERE id = ?")
366 .bind(&now)
367 .bind(key_id)
368 .execute(&self.pool)
369 .await
370 .map_err(|e| StarpodError::Auth(format!("Failed to revoke API key: {}", e)))?;
371 Ok(())
372 }
373
374 pub async fn link_telegram(
387 &self,
388 user_id: &str,
389 telegram_id: Option<i64>,
390 username: Option<&str>,
391 ) -> Result<TelegramLink> {
392 let now = Utc::now();
393 let now_str = now.to_rfc3339();
394
395 sqlx::query("DELETE FROM telegram_links WHERE user_id = ?")
397 .bind(user_id)
398 .execute(&self.pool)
399 .await
400 .map_err(|e| StarpodError::Auth(format!("Failed to clear old Telegram link: {}", e)))?;
401
402 if let Some(tid) = telegram_id {
405 sqlx::query("DELETE FROM telegram_links WHERE telegram_id = ?")
406 .bind(tid)
407 .execute(&self.pool)
408 .await
409 .map_err(|e| {
410 StarpodError::Auth(format!("Failed to clear old Telegram ID link: {}", e))
411 })?;
412 }
413 if let Some(uname) = username {
414 sqlx::query("DELETE FROM telegram_links WHERE username = ?")
415 .bind(uname)
416 .execute(&self.pool)
417 .await
418 .map_err(|e| {
419 StarpodError::Auth(format!("Failed to clear old username link: {}", e))
420 })?;
421 }
422
423 sqlx::query(
424 "INSERT INTO telegram_links (telegram_id, user_id, username, linked_at) \
425 VALUES (?, ?, ?, ?)",
426 )
427 .bind(telegram_id)
428 .bind(user_id)
429 .bind(username)
430 .bind(&now_str)
431 .execute(&self.pool)
432 .await
433 .map_err(|e| StarpodError::Auth(format!("Failed to link Telegram: {}", e)))?;
434
435 debug!(user_id = %user_id, telegram_id = ?telegram_id, username = ?username, "Telegram account linked");
436
437 Ok(TelegramLink {
438 telegram_id,
439 user_id: user_id.to_string(),
440 username: username.map(String::from),
441 linked_at: now,
442 })
443 }
444
445 pub async fn backfill_telegram_id(&self, username: &str, telegram_id: i64) -> Result<()> {
450 sqlx::query(
451 "UPDATE telegram_links SET telegram_id = ? WHERE username = ? AND telegram_id IS NULL",
452 )
453 .bind(telegram_id)
454 .bind(username)
455 .execute(&self.pool)
456 .await
457 .map_err(|e| StarpodError::Auth(format!("Failed to backfill Telegram ID: {}", e)))?;
458 Ok(())
459 }
460
461 pub async fn unlink_telegram(&self, telegram_id: i64) -> Result<()> {
463 sqlx::query("DELETE FROM telegram_links WHERE telegram_id = ?")
464 .bind(telegram_id)
465 .execute(&self.pool)
466 .await
467 .map_err(|e| StarpodError::Auth(format!("Failed to unlink Telegram: {}", e)))?;
468 Ok(())
469 }
470
471 pub async fn authenticate_telegram(
481 &self,
482 telegram_id: i64,
483 username: Option<&str>,
484 ) -> Result<Option<User>> {
485 let row = sqlx::query(
487 "SELECT u.id, u.email, u.display_name, u.role, u.is_active, u.filesystem_enabled, u.created_at, u.updated_at \
488 FROM telegram_links tl JOIN users u ON tl.user_id = u.id \
489 WHERE tl.telegram_id = ? AND u.is_active = 1"
490 )
491 .bind(telegram_id)
492 .fetch_optional(&self.pool)
493 .await
494 .map_err(|e| StarpodError::Auth(format!("Telegram auth query failed: {}", e)))?;
495
496 if let Some(r) = row {
497 return Ok(Some(row_to_user(&r)));
498 }
499
500 if let Some(uname) = username {
502 let row = sqlx::query(
503 "SELECT u.id, u.email, u.display_name, u.role, u.is_active, u.filesystem_enabled, u.created_at, u.updated_at \
504 FROM telegram_links tl JOIN users u ON tl.user_id = u.id \
505 WHERE tl.username = ? AND u.is_active = 1"
506 )
507 .bind(uname)
508 .fetch_optional(&self.pool)
509 .await
510 .map_err(|e| StarpodError::Auth(format!("Telegram username auth query failed: {}", e)))?;
511
512 if let Some(r) = row {
513 self.backfill_telegram_id(uname, telegram_id).await?;
515 return Ok(Some(row_to_user(&r)));
516 }
517 }
518
519 Ok(None)
520 }
521
522 pub async fn get_telegram_link_for_user(&self, user_id: &str) -> Result<Option<TelegramLink>> {
524 let row = sqlx::query(
525 "SELECT telegram_id, user_id, username, linked_at FROM telegram_links WHERE user_id = ?"
526 )
527 .bind(user_id)
528 .fetch_optional(&self.pool)
529 .await
530 .map_err(|e| StarpodError::Auth(format!("Failed to get Telegram link: {}", e)))?;
531
532 Ok(row.map(|r| TelegramLink {
533 telegram_id: r.get("telegram_id"),
534 user_id: r.get("user_id"),
535 username: r.get("username"),
536 linked_at: parse_dt(r.get("linked_at")),
537 }))
538 }
539
540 pub async fn unlink_telegram_by_user(&self, user_id: &str) -> Result<()> {
542 sqlx::query("DELETE FROM telegram_links WHERE user_id = ?")
543 .bind(user_id)
544 .execute(&self.pool)
545 .await
546 .map_err(|e| StarpodError::Auth(format!("Failed to unlink Telegram: {}", e)))?;
547 Ok(())
548 }
549
550 pub async fn list_telegram_links(&self) -> Result<Vec<TelegramLink>> {
552 let rows = sqlx::query(
553 "SELECT telegram_id, user_id, username, linked_at FROM telegram_links ORDER BY linked_at DESC"
554 )
555 .fetch_all(&self.pool)
556 .await
557 .map_err(|e| StarpodError::Auth(format!("Failed to list Telegram links: {}", e)))?;
558
559 Ok(rows
560 .iter()
561 .map(|r| TelegramLink {
562 telegram_id: r.get("telegram_id"),
563 user_id: r.get("user_id"),
564 username: r.get("username"),
565 linked_at: parse_dt(r.get("linked_at")),
566 })
567 .collect())
568 }
569
570 pub async fn log_event(
577 &self,
578 user_id: Option<&str>,
579 event_type: &str,
580 detail: Option<&str>,
581 ip_address: Option<&str>,
582 ) -> Result<()> {
583 let now = Utc::now().to_rfc3339();
584 sqlx::query(
585 "INSERT INTO auth_audit_log (user_id, event_type, detail, ip_address, created_at) \
586 VALUES (?, ?, ?, ?, ?)",
587 )
588 .bind(user_id)
589 .bind(event_type)
590 .bind(detail)
591 .bind(ip_address)
592 .bind(&now)
593 .execute(&self.pool)
594 .await
595 .map_err(|e| StarpodError::Auth(format!("Failed to log event: {}", e)))?;
596 Ok(())
597 }
598
599 pub async fn recent_audit(&self, limit: usize) -> Result<Vec<AuditEntry>> {
601 let rows = sqlx::query(
602 "SELECT id, user_id, event_type, detail, ip_address, created_at \
603 FROM auth_audit_log ORDER BY created_at DESC LIMIT ?",
604 )
605 .bind(limit as i64)
606 .fetch_all(&self.pool)
607 .await
608 .map_err(|e| StarpodError::Auth(format!("Failed to get audit log: {}", e)))?;
609
610 Ok(rows
611 .iter()
612 .map(|r| AuditEntry {
613 id: r.get("id"),
614 user_id: r.get("user_id"),
615 event_type: r.get("event_type"),
616 detail: r.get("detail"),
617 ip_address: r.get("ip_address"),
618 created_at: parse_dt(r.get("created_at")),
619 })
620 .collect())
621 }
622
623 pub async fn bootstrap_admin(
632 &self,
633 existing_api_key: Option<&str>,
634 ) -> Result<Option<(User, String)>> {
635 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
637 .fetch_one(&self.pool)
638 .await
639 .map_err(|e| StarpodError::Auth(format!("Count query failed: {}", e)))?;
640
641 if count > 0 {
642 return Ok(None); }
644
645 let admin = self.create_user(None, Some("Admin"), Role::Admin).await?;
646 self.update_user(&admin.id, None, None, None, Some(true))
648 .await?;
649 let admin = self.get_user(&admin.id).await?.unwrap_or(admin);
650
651 let key_str = if let Some(existing) = existing_api_key {
652 self.import_api_key(&admin.id, existing, Some("Imported from STARPOD_API_KEY"))
653 .await?;
654 info!("Imported existing STARPOD_API_KEY as admin API key");
655 existing.to_string()
656 } else {
657 let created = self
658 .create_api_key(&admin.id, Some("Auto-generated admin key"))
659 .await?;
660 info!(key = %created.key, "Generated new admin API key — save this!");
661 created.key
662 };
663
664 self.log_event(
665 Some(&admin.id),
666 "bootstrap",
667 Some("Admin user created"),
668 None,
669 )
670 .await?;
671
672 Ok(Some((admin, key_str)))
673 }
674
675 pub async fn has_users(&self) -> Result<bool> {
680 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
681 .fetch_one(&self.pool)
682 .await
683 .map_err(|e| StarpodError::Auth(format!("Count query failed: {}", e)))?;
684 Ok(count > 0)
685 }
686}
687
688fn row_to_user(row: &sqlx::sqlite::SqliteRow) -> User {
691 User {
692 id: row.get("id"),
693 email: row.get("email"),
694 display_name: row.get("display_name"),
695 role: Role::from_str(row.get::<&str, _>("role")).unwrap_or(Role::User),
696 is_active: row.get::<bool, _>("is_active"),
697 filesystem_enabled: row.get::<bool, _>("filesystem_enabled"),
698 created_at: parse_dt(row.get("created_at")),
699 updated_at: parse_dt(row.get("updated_at")),
700 }
701}
702
703fn row_to_api_key_meta(row: &sqlx::sqlite::SqliteRow) -> ApiKeyMeta {
704 ApiKeyMeta {
705 id: row.get("id"),
706 user_id: row.get("user_id"),
707 prefix: row.get("prefix"),
708 label: row.get("label"),
709 expires_at: row
710 .get::<Option<String>, _>("expires_at")
711 .and_then(|s| parse_dt_opt(&s)),
712 revoked_at: row
713 .get::<Option<String>, _>("revoked_at")
714 .and_then(|s| parse_dt_opt(&s)),
715 last_used_at: row
716 .get::<Option<String>, _>("last_used_at")
717 .and_then(|s| parse_dt_opt(&s)),
718 created_at: parse_dt(row.get("created_at")),
719 }
720}
721
722fn parse_dt(s: &str) -> chrono::DateTime<Utc> {
723 chrono::DateTime::parse_from_rfc3339(s)
724 .map(|dt| dt.with_timezone(&Utc))
725 .unwrap_or_else(|_| Utc::now())
726}
727
728fn parse_dt_opt(s: &str) -> Option<chrono::DateTime<Utc>> {
729 chrono::DateTime::parse_from_rfc3339(s)
730 .map(|dt| dt.with_timezone(&Utc))
731 .ok()
732}
733
734#[cfg(test)]
737mod tests {
738 use super::*;
739
740 async fn test_store() -> AuthStore {
741 let db = starpod_db::CoreDb::in_memory().await.unwrap();
742 AuthStore::from_pool(db.pool().clone())
743 }
744
745 #[tokio::test]
746 async fn create_and_get_user() {
747 let store = test_store().await;
748 let user = store
749 .create_user(Some("test@example.com"), Some("Test"), Role::User)
750 .await
751 .unwrap();
752 assert_eq!(user.role, Role::User);
753 assert!(user.is_active);
754
755 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
756 assert_eq!(fetched.id, user.id);
757 assert_eq!(fetched.email.as_deref(), Some("test@example.com"));
758 }
759
760 #[tokio::test]
761 async fn list_users() {
762 let store = test_store().await;
763 store
764 .create_user(None, Some("A"), Role::Admin)
765 .await
766 .unwrap();
767 store
768 .create_user(None, Some("B"), Role::User)
769 .await
770 .unwrap();
771
772 let users = store.list_users().await.unwrap();
773 assert_eq!(users.len(), 2);
774 }
775
776 #[tokio::test]
777 async fn update_user() {
778 let store = test_store().await;
779 let user = store
780 .create_user(None, Some("Old"), Role::User)
781 .await
782 .unwrap();
783 store
784 .update_user(&user.id, Some("new@example.com"), Some("New"), None, None)
785 .await
786 .unwrap();
787
788 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
789 assert_eq!(fetched.email.as_deref(), Some("new@example.com"));
790 assert_eq!(fetched.display_name.as_deref(), Some("New"));
791 }
792
793 #[tokio::test]
794 async fn deactivate_user() {
795 let store = test_store().await;
796 let user = store.create_user(None, None, Role::User).await.unwrap();
797 store.deactivate_user(&user.id).await.unwrap();
798
799 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
800 assert!(!fetched.is_active);
801 }
802
803 #[tokio::test]
804 async fn api_key_create_and_authenticate() {
805 let store = test_store().await;
806 let user = store.create_user(None, None, Role::User).await.unwrap();
807 let created = store
808 .create_api_key(&user.id, Some("test key"))
809 .await
810 .unwrap();
811
812 assert!(created.key.starts_with("sp_live_"));
813 assert_eq!(created.meta.label.as_deref(), Some("test key"));
814
815 let authed = store
816 .authenticate_api_key(&created.key)
817 .await
818 .unwrap()
819 .unwrap();
820 assert_eq!(authed.id, user.id);
821 }
822
823 #[tokio::test]
824 async fn api_key_wrong_key_fails() {
825 let store = test_store().await;
826 let user = store.create_user(None, None, Role::User).await.unwrap();
827 store.create_api_key(&user.id, None).await.unwrap();
828
829 let result = store
830 .authenticate_api_key("sp_live_0000000000000000000000000000000000000000")
831 .await
832 .unwrap();
833 assert!(result.is_none());
834 }
835
836 #[tokio::test]
837 async fn revoked_key_fails_auth() {
838 let store = test_store().await;
839 let user = store.create_user(None, None, Role::User).await.unwrap();
840 let created = store.create_api_key(&user.id, None).await.unwrap();
841
842 store.revoke_api_key(&created.meta.id).await.unwrap();
843
844 let result = store.authenticate_api_key(&created.key).await.unwrap();
845 assert!(result.is_none());
846 }
847
848 #[tokio::test]
849 async fn deactivated_user_fails_auth() {
850 let store = test_store().await;
851 let user = store.create_user(None, None, Role::User).await.unwrap();
852 let created = store.create_api_key(&user.id, None).await.unwrap();
853
854 store.deactivate_user(&user.id).await.unwrap();
855
856 let result = store.authenticate_api_key(&created.key).await.unwrap();
857 assert!(result.is_none());
858 }
859
860 #[tokio::test]
861 async fn list_api_keys() {
862 let store = test_store().await;
863 let user = store.create_user(None, None, Role::User).await.unwrap();
864 store.create_api_key(&user.id, Some("key1")).await.unwrap();
865 store.create_api_key(&user.id, Some("key2")).await.unwrap();
866
867 let keys = store.list_api_keys(&user.id).await.unwrap();
868 assert_eq!(keys.len(), 2);
869 }
870
871 #[tokio::test]
872 async fn telegram_link_and_auth() {
873 let store = test_store().await;
874 let user = store.create_user(None, None, Role::User).await.unwrap();
875 store
876 .link_telegram(&user.id, Some(123456789), Some("alice"))
877 .await
878 .unwrap();
879
880 let authed = store
881 .authenticate_telegram(123456789, None)
882 .await
883 .unwrap()
884 .unwrap();
885 assert_eq!(authed.id, user.id);
886 }
887
888 #[tokio::test]
889 async fn telegram_unlinked_fails() {
890 let store = test_store().await;
891 let result = store.authenticate_telegram(999999, None).await.unwrap();
892 assert!(result.is_none());
893 }
894
895 #[tokio::test]
896 async fn telegram_unlink() {
897 let store = test_store().await;
898 let user = store.create_user(None, None, Role::User).await.unwrap();
899 store
900 .link_telegram(&user.id, Some(123), None)
901 .await
902 .unwrap();
903 store.unlink_telegram(123).await.unwrap();
904
905 let result = store.authenticate_telegram(123, None).await.unwrap();
906 assert!(result.is_none());
907 }
908
909 #[tokio::test]
910 async fn list_telegram_links() {
911 let store = test_store().await;
912 let alice = store
913 .create_user(None, Some("Alice"), Role::User)
914 .await
915 .unwrap();
916 let bob = store
917 .create_user(None, Some("Bob"), Role::User)
918 .await
919 .unwrap();
920 store
921 .link_telegram(&alice.id, Some(111), Some("alice"))
922 .await
923 .unwrap();
924 store
925 .link_telegram(&bob.id, Some(222), Some("bob"))
926 .await
927 .unwrap();
928
929 let links = store.list_telegram_links().await.unwrap();
930 assert_eq!(links.len(), 2);
931 }
932
933 #[tokio::test]
934 async fn audit_log() {
935 let store = test_store().await;
936 store
937 .log_event(
938 Some("user1"),
939 "login",
940 Some("via API key"),
941 Some("127.0.0.1"),
942 )
943 .await
944 .unwrap();
945 store
946 .log_event(None, "failed_auth", Some("invalid key"), Some("1.2.3.4"))
947 .await
948 .unwrap();
949
950 let entries = store.recent_audit(10).await.unwrap();
951 assert_eq!(entries.len(), 2);
952 assert_eq!(entries[0].event_type, "failed_auth"); }
954
955 #[tokio::test]
956 async fn bootstrap_admin_creates_user_and_key() {
957 let store = test_store().await;
958 let result = store.bootstrap_admin(None).await.unwrap();
959 assert!(result.is_some());
960
961 let (admin, key) = result.unwrap();
962 assert_eq!(admin.role, Role::Admin);
963 assert!(key.starts_with("sp_live_"));
964
965 let result2 = store.bootstrap_admin(None).await.unwrap();
967 assert!(result2.is_none());
968 }
969
970 #[tokio::test]
971 async fn bootstrap_admin_with_existing_key() {
972 let store = test_store().await;
973 let legacy_key = "my-old-secret-key";
974 let result = store.bootstrap_admin(Some(legacy_key)).await.unwrap();
975 assert!(result.is_some());
976
977 let (_, returned_key) = result.unwrap();
978 assert_eq!(returned_key, legacy_key);
979
980 let authed = store.authenticate_api_key(legacy_key).await.unwrap();
982 assert!(authed.is_some());
983 assert_eq!(authed.unwrap().role, Role::Admin);
984 }
985
986 #[tokio::test]
987 async fn update_user_role() {
988 let store = test_store().await;
989 let user = store.create_user(None, None, Role::User).await.unwrap();
990 store
991 .update_user(&user.id, None, None, Some(Role::Admin), None)
992 .await
993 .unwrap();
994
995 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
996 assert_eq!(fetched.role, Role::Admin);
997 }
998
999 #[tokio::test]
1000 async fn has_users_empty() {
1001 let store = test_store().await;
1002 assert!(!store.has_users().await.unwrap());
1003 }
1004
1005 #[tokio::test]
1006 async fn has_users_with_user() {
1007 let store = test_store().await;
1008 store.create_user(None, None, Role::User).await.unwrap();
1009 assert!(store.has_users().await.unwrap());
1010 }
1011
1012 #[tokio::test]
1013 async fn duplicate_email_rejected() {
1014 let store = test_store().await;
1015 store
1016 .create_user(Some("dup@example.com"), None, Role::User)
1017 .await
1018 .unwrap();
1019 let result = store
1020 .create_user(Some("dup@example.com"), None, Role::User)
1021 .await;
1022 assert!(
1023 result.is_err(),
1024 "Duplicate email should be rejected by UNIQUE constraint"
1025 );
1026 }
1027
1028 #[tokio::test]
1029 async fn null_email_allows_multiple() {
1030 let store = test_store().await;
1031 store.create_user(None, None, Role::User).await.unwrap();
1032 store.create_user(None, None, Role::User).await.unwrap();
1033 let users = store.list_users().await.unwrap();
1034 assert_eq!(
1035 users.len(),
1036 2,
1037 "Multiple users with NULL email should be allowed"
1038 );
1039 }
1040
1041 #[tokio::test]
1042 async fn get_nonexistent_user() {
1043 let store = test_store().await;
1044 let result = store.get_user("nonexistent-id").await.unwrap();
1045 assert!(result.is_none());
1046 }
1047
1048 #[tokio::test]
1049 async fn authenticate_empty_key() {
1050 let store = test_store().await;
1051 let user = store.create_user(None, None, Role::User).await.unwrap();
1052 store.create_api_key(&user.id, None).await.unwrap();
1053 let result = store.authenticate_api_key("").await.unwrap();
1054 assert!(result.is_none());
1055 }
1056
1057 #[tokio::test]
1058 async fn authenticate_updates_last_used() {
1059 let store = test_store().await;
1060 let user = store.create_user(None, None, Role::User).await.unwrap();
1061 let created = store.create_api_key(&user.id, None).await.unwrap();
1062
1063 let keys = store.list_api_keys(&user.id).await.unwrap();
1065 assert!(keys[0].last_used_at.is_none());
1066
1067 store.authenticate_api_key(&created.key).await.unwrap();
1069 let keys = store.list_api_keys(&user.id).await.unwrap();
1070 assert!(keys[0].last_used_at.is_some());
1071 }
1072
1073 #[tokio::test]
1074 async fn multiple_keys_per_user() {
1075 let store = test_store().await;
1076 let user = store.create_user(None, None, Role::User).await.unwrap();
1077 let k1 = store.create_api_key(&user.id, Some("key1")).await.unwrap();
1078 let k2 = store.create_api_key(&user.id, Some("key2")).await.unwrap();
1079
1080 let u1 = store.authenticate_api_key(&k1.key).await.unwrap().unwrap();
1082 let u2 = store.authenticate_api_key(&k2.key).await.unwrap().unwrap();
1083 assert_eq!(u1.id, user.id);
1084 assert_eq!(u2.id, user.id);
1085
1086 store.revoke_api_key(&k1.meta.id).await.unwrap();
1088 assert!(store.authenticate_api_key(&k1.key).await.unwrap().is_none());
1089 assert!(store.authenticate_api_key(&k2.key).await.unwrap().is_some());
1090 }
1091
1092 #[tokio::test]
1093 async fn telegram_relink_to_different_user() {
1094 let store = test_store().await;
1095 let alice = store
1096 .create_user(None, Some("Alice"), Role::User)
1097 .await
1098 .unwrap();
1099 let bob = store
1100 .create_user(None, Some("Bob"), Role::User)
1101 .await
1102 .unwrap();
1103
1104 store
1106 .link_telegram(&alice.id, Some(999), None)
1107 .await
1108 .unwrap();
1109 let authed = store
1110 .authenticate_telegram(999, None)
1111 .await
1112 .unwrap()
1113 .unwrap();
1114 assert_eq!(authed.id, alice.id);
1115
1116 store.link_telegram(&bob.id, Some(999), None).await.unwrap();
1118 let authed = store
1119 .authenticate_telegram(999, None)
1120 .await
1121 .unwrap()
1122 .unwrap();
1123 assert_eq!(authed.id, bob.id, "Relink should point to the new user");
1124
1125 let links = store.list_telegram_links().await.unwrap();
1127 assert_eq!(links.len(), 1);
1128 }
1129
1130 #[tokio::test]
1131 async fn deactivated_user_telegram_auth_fails() {
1132 let store = test_store().await;
1133 let user = store.create_user(None, None, Role::User).await.unwrap();
1134 store
1135 .link_telegram(&user.id, Some(111), None)
1136 .await
1137 .unwrap();
1138
1139 store.deactivate_user(&user.id).await.unwrap();
1140
1141 let result = store.authenticate_telegram(111, None).await.unwrap();
1142 assert!(
1143 result.is_none(),
1144 "Deactivated user should not authenticate via Telegram"
1145 );
1146 }
1147
1148 #[tokio::test]
1149 async fn audit_log_entries_have_correct_fields() {
1150 let store = test_store().await;
1151 store
1152 .log_event(
1153 Some("uid"),
1154 "api_key_created",
1155 Some("label: test"),
1156 Some("10.0.0.1"),
1157 )
1158 .await
1159 .unwrap();
1160
1161 let entries = store.recent_audit(1).await.unwrap();
1162 assert_eq!(entries.len(), 1);
1163 let e = &entries[0];
1164 assert_eq!(e.user_id.as_deref(), Some("uid"));
1165 assert_eq!(e.event_type, "api_key_created");
1166 assert_eq!(e.detail.as_deref(), Some("label: test"));
1167 assert_eq!(e.ip_address.as_deref(), Some("10.0.0.1"));
1168 }
1169
1170 #[tokio::test]
1171 async fn audit_log_respects_limit() {
1172 let store = test_store().await;
1173 for i in 0..10 {
1174 store
1175 .log_event(None, &format!("event_{}", i), None, None)
1176 .await
1177 .unwrap();
1178 }
1179 let entries = store.recent_audit(3).await.unwrap();
1180 assert_eq!(entries.len(), 3);
1181 }
1182
1183 #[tokio::test]
1184 async fn get_telegram_link_for_user() {
1185 let store = test_store().await;
1186 let user = store
1187 .create_user(None, Some("Alice"), Role::User)
1188 .await
1189 .unwrap();
1190 store
1191 .link_telegram(&user.id, Some(12345), Some("alice"))
1192 .await
1193 .unwrap();
1194
1195 let link = store
1196 .get_telegram_link_for_user(&user.id)
1197 .await
1198 .unwrap()
1199 .unwrap();
1200 assert_eq!(link.telegram_id, Some(12345));
1201 assert_eq!(link.username.as_deref(), Some("alice"));
1202 }
1203
1204 #[tokio::test]
1205 async fn get_telegram_link_for_user_none() {
1206 let store = test_store().await;
1207 let user = store.create_user(None, None, Role::User).await.unwrap();
1208 let link = store.get_telegram_link_for_user(&user.id).await.unwrap();
1209 assert!(link.is_none());
1210 }
1211
1212 #[tokio::test]
1213 async fn unlink_telegram_by_user() {
1214 let store = test_store().await;
1215 let user = store.create_user(None, None, Role::User).await.unwrap();
1216 store
1217 .link_telegram(&user.id, Some(999), None)
1218 .await
1219 .unwrap();
1220
1221 store.unlink_telegram_by_user(&user.id).await.unwrap();
1222 let result = store.authenticate_telegram(999, None).await.unwrap();
1223 assert!(result.is_none());
1224 }
1225
1226 #[tokio::test]
1227 async fn telegram_link_username_only() {
1228 let store = test_store().await;
1229 let user = store
1230 .create_user(None, Some("Alice"), Role::User)
1231 .await
1232 .unwrap();
1233
1234 let link = store
1236 .link_telegram(&user.id, None, Some("alice"))
1237 .await
1238 .unwrap();
1239 assert_eq!(link.telegram_id, None);
1240 assert_eq!(link.username.as_deref(), Some("alice"));
1241
1242 let found = store
1244 .get_telegram_link_for_user(&user.id)
1245 .await
1246 .unwrap()
1247 .unwrap();
1248 assert_eq!(found.telegram_id, None);
1249 assert_eq!(found.username.as_deref(), Some("alice"));
1250 }
1251
1252 #[tokio::test]
1253 async fn telegram_auth_by_username_with_backfill() {
1254 let store = test_store().await;
1255 let user = store
1256 .create_user(None, Some("Alice"), Role::User)
1257 .await
1258 .unwrap();
1259
1260 store
1262 .link_telegram(&user.id, None, Some("alice"))
1263 .await
1264 .unwrap();
1265
1266 let result = store.authenticate_telegram(42, None).await.unwrap();
1268 assert!(result.is_none());
1269
1270 let authed = store
1272 .authenticate_telegram(42, Some("alice"))
1273 .await
1274 .unwrap()
1275 .unwrap();
1276 assert_eq!(authed.id, user.id);
1277
1278 let authed2 = store
1280 .authenticate_telegram(42, None)
1281 .await
1282 .unwrap()
1283 .unwrap();
1284 assert_eq!(authed2.id, user.id);
1285 }
1286
1287 #[tokio::test]
1288 async fn telegram_auth_username_mismatch_fails() {
1289 let store = test_store().await;
1290 let user = store.create_user(None, None, Role::User).await.unwrap();
1291
1292 store
1294 .link_telegram(&user.id, None, Some("alice"))
1295 .await
1296 .unwrap();
1297
1298 let result = store.authenticate_telegram(42, Some("bob")).await.unwrap();
1300 assert!(result.is_none());
1301 }
1302
1303 #[tokio::test]
1304 async fn telegram_link_replaces_same_user() {
1305 let store = test_store().await;
1306 let user = store.create_user(None, None, Role::User).await.unwrap();
1307
1308 store
1310 .link_telegram(&user.id, Some(111), Some("old"))
1311 .await
1312 .unwrap();
1313 store
1315 .link_telegram(&user.id, Some(222), Some("new"))
1316 .await
1317 .unwrap();
1318
1319 let result = store.authenticate_telegram(111, None).await.unwrap();
1320 assert!(result.is_none(), "Old telegram_id should no longer work");
1321
1322 let authed = store
1323 .authenticate_telegram(222, None)
1324 .await
1325 .unwrap()
1326 .unwrap();
1327 assert_eq!(authed.id, user.id);
1328
1329 let links = store.list_telegram_links().await.unwrap();
1331 assert_eq!(links.len(), 1);
1332 }
1333
1334 #[tokio::test]
1335 async fn telegram_link_replaces_conflicting_username() {
1336 let store = test_store().await;
1337 let alice = store
1338 .create_user(None, Some("Alice"), Role::User)
1339 .await
1340 .unwrap();
1341 let bob = store
1342 .create_user(None, Some("Bob"), Role::User)
1343 .await
1344 .unwrap();
1345
1346 store
1348 .link_telegram(&alice.id, None, Some("shared"))
1349 .await
1350 .unwrap();
1351 store
1353 .link_telegram(&bob.id, None, Some("shared"))
1354 .await
1355 .unwrap();
1356
1357 let authed = store
1358 .authenticate_telegram(42, Some("shared"))
1359 .await
1360 .unwrap()
1361 .unwrap();
1362 assert_eq!(
1363 authed.id, bob.id,
1364 "Username 'shared' should now point to Bob"
1365 );
1366
1367 let alice_link = store.get_telegram_link_for_user(&alice.id).await.unwrap();
1369 assert!(alice_link.is_none());
1370 }
1371
1372 #[tokio::test]
1373 async fn telegram_backfill_does_not_overwrite_existing_id() {
1374 let store = test_store().await;
1375 let user = store.create_user(None, None, Role::User).await.unwrap();
1376
1377 store
1379 .link_telegram(&user.id, Some(100), Some("alice"))
1380 .await
1381 .unwrap();
1382
1383 store.backfill_telegram_id("alice", 999).await.unwrap();
1385
1386 let authed = store
1388 .authenticate_telegram(100, None)
1389 .await
1390 .unwrap()
1391 .unwrap();
1392 assert_eq!(authed.id, user.id);
1393
1394 let result = store.authenticate_telegram(999, None).await.unwrap();
1396 assert!(result.is_none());
1397 }
1398
1399 #[test]
1400 fn role_display() {
1401 assert_eq!(Role::Admin.to_string(), "admin");
1402 assert_eq!(Role::User.to_string(), "user");
1403 }
1404
1405 #[test]
1406 fn role_from_str() {
1407 assert_eq!(Role::from_str("admin"), Some(Role::Admin));
1408 assert_eq!(Role::from_str("user"), Some(Role::User));
1409 assert_eq!(Role::from_str("unknown"), None);
1410 }
1411
1412 #[test]
1413 fn role_serde_roundtrip() {
1414 let json = serde_json::to_string(&Role::Admin).unwrap();
1415 assert_eq!(json, "\"admin\"");
1416 let parsed: Role = serde_json::from_str(&json).unwrap();
1417 assert_eq!(parsed, Role::Admin);
1418 }
1419
1420 #[test]
1421 fn user_serializes_correctly() {
1422 let user = User {
1423 id: "test-id".into(),
1424 email: Some("test@example.com".into()),
1425 display_name: Some("Test".into()),
1426 role: Role::User,
1427 is_active: true,
1428 filesystem_enabled: false,
1429 created_at: Utc::now(),
1430 updated_at: Utc::now(),
1431 };
1432 let json = serde_json::to_string(&user).unwrap();
1433 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1434 assert_eq!(parsed["id"], "test-id");
1435 assert_eq!(parsed["role"], "user");
1436 assert_eq!(parsed["is_active"], true);
1437 assert_eq!(parsed["filesystem_enabled"], false);
1438 }
1439
1440 #[tokio::test]
1443 async fn create_user_defaults_filesystem_disabled() {
1444 let store = test_store().await;
1445 let user = store
1446 .create_user(None, Some("Test"), Role::User)
1447 .await
1448 .unwrap();
1449 assert!(!user.filesystem_enabled);
1450
1451 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
1452 assert!(!fetched.filesystem_enabled);
1453 }
1454
1455 #[tokio::test]
1456 async fn update_user_enables_filesystem() {
1457 let store = test_store().await;
1458 let user = store
1459 .create_user(None, Some("Test"), Role::User)
1460 .await
1461 .unwrap();
1462 assert!(!user.filesystem_enabled);
1463
1464 store
1465 .update_user(&user.id, None, None, None, Some(true))
1466 .await
1467 .unwrap();
1468 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
1469 assert!(fetched.filesystem_enabled);
1470 }
1471
1472 #[tokio::test]
1473 async fn update_user_disables_filesystem() {
1474 let store = test_store().await;
1475 let user = store
1476 .create_user(None, Some("Test"), Role::User)
1477 .await
1478 .unwrap();
1479 store
1480 .update_user(&user.id, None, None, None, Some(true))
1481 .await
1482 .unwrap();
1483
1484 store
1485 .update_user(&user.id, None, None, None, Some(false))
1486 .await
1487 .unwrap();
1488 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
1489 assert!(!fetched.filesystem_enabled);
1490 }
1491
1492 #[tokio::test]
1493 async fn update_user_none_preserves_filesystem() {
1494 let store = test_store().await;
1495 let user = store
1496 .create_user(None, Some("Test"), Role::User)
1497 .await
1498 .unwrap();
1499 store
1500 .update_user(&user.id, None, None, None, Some(true))
1501 .await
1502 .unwrap();
1503
1504 store
1506 .update_user(&user.id, None, Some("NewName"), None, None)
1507 .await
1508 .unwrap();
1509 let fetched = store.get_user(&user.id).await.unwrap().unwrap();
1510 assert!(fetched.filesystem_enabled);
1511 assert_eq!(fetched.display_name.as_deref(), Some("NewName"));
1512 }
1513
1514 #[tokio::test]
1515 async fn list_users_includes_filesystem_field() {
1516 let store = test_store().await;
1517 let u1 = store
1518 .create_user(None, Some("A"), Role::User)
1519 .await
1520 .unwrap();
1521 store
1522 .create_user(None, Some("B"), Role::User)
1523 .await
1524 .unwrap();
1525 store
1526 .update_user(&u1.id, None, None, None, Some(true))
1527 .await
1528 .unwrap();
1529
1530 let users = store.list_users().await.unwrap();
1531 assert_eq!(users.len(), 2);
1532 let a = users
1533 .iter()
1534 .find(|u| u.display_name.as_deref() == Some("A"))
1535 .unwrap();
1536 let b = users
1537 .iter()
1538 .find(|u| u.display_name.as_deref() == Some("B"))
1539 .unwrap();
1540 assert!(a.filesystem_enabled);
1541 assert!(!b.filesystem_enabled);
1542 }
1543
1544 #[tokio::test]
1545 async fn bootstrap_admin_has_filesystem_enabled() {
1546 let store = test_store().await;
1547 let result = store.bootstrap_admin(None).await.unwrap();
1548 let (admin, _key) = result.unwrap();
1549 assert!(
1550 admin.filesystem_enabled,
1551 "Bootstrap admin should have filesystem enabled"
1552 );
1553
1554 let fetched = store.get_user(&admin.id).await.unwrap().unwrap();
1556 assert!(fetched.filesystem_enabled);
1557 }
1558
1559 #[tokio::test]
1560 async fn api_key_auth_returns_filesystem_field() {
1561 let store = test_store().await;
1562 let user = store.create_user(None, None, Role::User).await.unwrap();
1563 store
1564 .update_user(&user.id, None, None, None, Some(true))
1565 .await
1566 .unwrap();
1567 let key = store.create_api_key(&user.id, None).await.unwrap();
1568
1569 let authed = store.authenticate_api_key(&key.key).await.unwrap().unwrap();
1570 assert!(authed.filesystem_enabled);
1571 }
1572
1573 #[tokio::test]
1574 async fn telegram_auth_returns_filesystem_field() {
1575 let store = test_store().await;
1576 let user = store.create_user(None, None, Role::User).await.unwrap();
1577 store
1578 .update_user(&user.id, None, None, None, Some(true))
1579 .await
1580 .unwrap();
1581 store
1582 .link_telegram(&user.id, Some(12345), None)
1583 .await
1584 .unwrap();
1585
1586 let authed = store
1587 .authenticate_telegram(12345, None)
1588 .await
1589 .unwrap()
1590 .unwrap();
1591 assert!(authed.filesystem_enabled);
1592 }
1593
1594 #[test]
1595 fn user_serialization_includes_filesystem_enabled() {
1596 let user = User {
1597 id: "u1".into(),
1598 email: None,
1599 display_name: None,
1600 role: Role::Admin,
1601 is_active: true,
1602 filesystem_enabled: true,
1603 created_at: Utc::now(),
1604 updated_at: Utc::now(),
1605 };
1606 let json = serde_json::to_string(&user).unwrap();
1607 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1608 assert_eq!(parsed["filesystem_enabled"], true);
1609 }
1610}