1use axum::Json;
2use axum::extract::{Path, State};
3use axum::http::StatusCode;
4use axum_extra::extract::PrivateCookieJar;
5use axum_extra::extract::cookie::Cookie;
6use kellnr_appstate::{AppState, DbState, TokenCacheState};
7use kellnr_auth::token;
8use kellnr_common::util::generate_rand_string;
9use kellnr_db::password::generate_salt;
10use kellnr_db::{self, AuthToken, User};
11use kellnr_settings::constants::{COOKIE_SESSION_ID, COOKIE_SESSION_USER};
12use serde::{Deserialize, Serialize};
13use utoipa::ToSchema;
14
15use crate::error::RouteError;
16use crate::session::{AdminUser, MaybeUser, create_session_jar};
17
18#[derive(Serialize, ToSchema)]
19pub struct NewTokenResponse {
20 name: String,
21 token: String,
22}
23
24#[utoipa::path(
26 post,
27 path = "/me/tokens",
28 tag = "users",
29 request_body = token::NewTokenReqData,
30 responses(
31 (status = 200, description = "Token created successfully", body = NewTokenResponse),
32 (status = 401, description = "Not authenticated")
33 ),
34 security(("session_cookie" = []))
35)]
36pub async fn add_token(
37 user: MaybeUser,
38 State(db): DbState,
39 State(cache): TokenCacheState,
40 Json(auth_token): Json<token::NewTokenReqData>,
41) -> Result<Json<NewTokenResponse>, RouteError> {
42 let token = token::generate_token();
43 db.add_auth_token(&auth_token.name, &token, user.name())
44 .await?;
45
46 cache.invalidate_all();
47
48 Ok(NewTokenResponse {
49 name: auth_token.name.clone(),
50 token,
51 }
52 .into())
53}
54
55#[utoipa::path(
57 get,
58 path = "/me/tokens",
59 tag = "users",
60 responses(
61 (status = 200, description = "List of auth tokens", body = Vec<AuthToken>),
62 (status = 401, description = "Not authenticated")
63 ),
64 security(("session_cookie" = []))
65)]
66pub async fn list_tokens(
67 user: MaybeUser,
68 State(db): DbState,
69) -> Result<Json<Vec<AuthToken>>, RouteError> {
70 Ok(Json(db.get_auth_tokens(user.name()).await?))
71}
72
73#[utoipa::path(
75 get,
76 path = "/",
77 tag = "users",
78 responses(
79 (status = 200, description = "List of all users", body = Vec<User>),
80 (status = 403, description = "Admin access required")
81 ),
82 security(("session_cookie" = []))
83)]
84pub async fn list_users(
85 _user: AdminUser,
86 State(db): DbState,
87) -> Result<Json<Vec<User>>, RouteError> {
88 Ok(Json(db.get_users().await?))
89}
90
91#[utoipa::path(
93 delete,
94 path = "/me/tokens/{id}",
95 tag = "users",
96 params(
97 ("id" = i32, Path, description = "Token ID to delete")
98 ),
99 responses(
100 (status = 200, description = "Token deleted successfully"),
101 (status = 400, description = "Token not found"),
102 (status = 401, description = "Not authenticated")
103 ),
104 security(("session_cookie" = []))
105)]
106pub async fn delete_token(
107 user: MaybeUser,
108 Path(id): Path<i32>,
109 State(db): DbState,
110 State(cache): TokenCacheState,
111) -> Result<(), RouteError> {
112 db.get_auth_tokens(user.name())
113 .await?
114 .iter()
115 .find(|t| t.id == id)
116 .ok_or_else(|| RouteError::Status(StatusCode::BAD_REQUEST))?;
117
118 db.delete_auth_token(id).await?;
119
120 cache.invalidate_all();
121
122 Ok(())
123}
124
125#[derive(Serialize, ToSchema)]
126pub struct ResetPwd {
127 new_pwd: String,
128 user: String,
129}
130
131#[utoipa::path(
133 put,
134 path = "/{name}/password",
135 tag = "users",
136 params(
137 ("name" = String, Path, description = "Username")
138 ),
139 responses(
140 (status = 200, description = "Password reset successfully", body = ResetPwd),
141 (status = 403, description = "Admin access required")
142 ),
143 security(("session_cookie" = []))
144)]
145pub async fn reset_pwd(
146 user: AdminUser,
147 Path(name): Path<String>,
148 State(db): DbState,
149) -> Result<Json<ResetPwd>, RouteError> {
150 let new_pwd = generate_rand_string(12);
151 db.change_pwd(&name, &new_pwd).await?;
152
153 Ok(ResetPwd {
154 user: user.name().to_owned(),
155 new_pwd,
156 }
157 .into())
158}
159
160#[derive(Deserialize, ToSchema)]
161pub struct ReadOnlyState {
162 pub state: bool,
163}
164
165#[utoipa::path(
167 post,
168 path = "/{name}/read-only",
169 tag = "users",
170 params(
171 ("name" = String, Path, description = "Username")
172 ),
173 request_body = ReadOnlyState,
174 responses(
175 (status = 200, description = "Read-only state changed successfully"),
176 (status = 400, description = "Cannot lock yourself"),
177 (status = 403, description = "Admin access required")
178 ),
179 security(("session_cookie" = []))
180)]
181pub async fn read_only(
182 user: AdminUser,
183 Path(name): Path<String>,
184 State(db): DbState,
185 State(cache): TokenCacheState,
186 Json(ro_state): Json<ReadOnlyState>,
187) -> Result<(), RouteError> {
188 if user.name() == name && ro_state.state {
190 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
191 }
192
193 db.change_read_only_state(&name, ro_state.state).await?;
194
195 cache.invalidate_all();
196
197 Ok(())
198}
199
200#[derive(Deserialize, ToSchema)]
201pub struct AdminState {
202 pub state: bool,
203}
204
205#[utoipa::path(
207 post,
208 path = "/{name}/admin",
209 tag = "users",
210 params(
211 ("name" = String, Path, description = "Username")
212 ),
213 request_body = AdminState,
214 responses(
215 (status = 200, description = "Admin state changed successfully"),
216 (status = 400, description = "Cannot demote yourself"),
217 (status = 403, description = "Admin access required")
218 ),
219 security(("session_cookie" = []))
220)]
221pub async fn admin(
222 user: AdminUser,
223 Path(name): Path<String>,
224 State(db): DbState,
225 State(cache): TokenCacheState,
226 Json(admin_state): Json<AdminState>,
227) -> Result<(), RouteError> {
228 if user.name() == name && !admin_state.state {
230 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
231 }
232
233 db.change_admin_state(&name, admin_state.state).await?;
234
235 cache.invalidate_all();
236
237 Ok(())
238}
239
240#[utoipa::path(
242 delete,
243 path = "/{name}",
244 tag = "users",
245 params(
246 ("name" = String, Path, description = "Username to delete")
247 ),
248 responses(
249 (status = 200, description = "User deleted successfully"),
250 (status = 400, description = "Cannot delete yourself"),
251 (status = 403, description = "Admin access required")
252 ),
253 security(("session_cookie" = []))
254)]
255pub async fn delete(
256 user: AdminUser,
257 Path(name): Path<String>,
258 State(db): DbState,
259 State(cache): TokenCacheState,
260) -> Result<(), RouteError> {
261 if user.name() == name {
263 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
264 }
265
266 db.delete_user(&name).await?;
267
268 cache.invalidate_all();
269
270 Ok(())
271}
272
273#[derive(Serialize, ToSchema)]
274pub struct LoggedInUser {
275 user: String,
276 is_admin: bool,
277 is_logged_in: bool,
278}
279
280#[derive(Deserialize, ToSchema)]
281pub struct Credentials {
282 pub user: String,
283 pub pwd: String,
284}
285
286impl Credentials {
287 pub fn validate(&self) -> Result<(), RouteError> {
288 if self.user.is_empty() {
289 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
290 }
291 if self.pwd.is_empty() {
292 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
293 }
294 Ok(())
295 }
296}
297
298#[utoipa::path(
300 post,
301 path = "/login",
302 tag = "auth",
303 request_body = Credentials,
304 responses(
305 (status = 200, description = "Successfully logged in", body = LoggedInUser),
306 (status = 400, description = "Invalid credentials"),
307 (status = 401, description = "Authentication failed")
308 )
309)]
310pub async fn login(
311 cookies: PrivateCookieJar,
312 State(state): AppState,
313 Json(credentials): Json<Credentials>,
314) -> Result<(PrivateCookieJar, Json<LoggedInUser>), RouteError> {
315 credentials.validate()?;
316
317 let user = state
318 .db
319 .authenticate_user(&credentials.user, &credentials.pwd)
320 .await
321 .map_err(|_| RouteError::AuthenticationFailure)?;
322
323 let jar = create_session_jar(cookies, &state, &credentials.user).await?;
324
325 Ok((
326 jar,
327 LoggedInUser {
328 user: credentials.user.clone(),
329 is_admin: user.is_admin,
330 is_logged_in: true,
331 }
332 .into(),
333 ))
334}
335
336#[utoipa::path(
338 get,
339 path = "/state",
340 tag = "auth",
341 responses(
342 (status = 200, description = "Current login state", body = LoggedInUser)
343 )
344)]
345#[expect(clippy::unused_async)] pub async fn login_state(user: Option<MaybeUser>) -> Json<LoggedInUser> {
347 match user {
348 Some(MaybeUser::Normal(user)) => LoggedInUser {
349 user,
350 is_admin: false,
351 is_logged_in: true,
352 },
353 Some(MaybeUser::Admin(user)) => LoggedInUser {
354 user,
355 is_admin: true,
356 is_logged_in: true,
357 },
358 None => LoggedInUser {
359 user: String::new(),
360 is_admin: false,
361 is_logged_in: false,
362 },
363 }
364 .into()
365}
366
367#[utoipa::path(
369 post,
370 path = "/logout",
371 tag = "auth",
372 responses(
373 (status = 200, description = "Successfully logged out")
374 )
375)]
376pub async fn logout(
377 mut jar: PrivateCookieJar,
378 State(state): AppState,
379) -> Result<PrivateCookieJar, RouteError> {
380 let session_id = match jar.get(COOKIE_SESSION_ID) {
381 Some(c) => c.value().to_owned(),
382 None => return Ok(jar), };
384
385 jar = jar.remove(COOKIE_SESSION_ID);
386 jar = jar.remove(Cookie::build((COOKIE_SESSION_USER, "")).path("/"));
387
388 state.db.delete_session_token(&session_id).await?;
389 Ok(jar)
390}
391
392#[derive(Deserialize, ToSchema)]
393pub struct PwdChange {
394 pub old_pwd: String,
395 pub new_pwd1: String,
396 pub new_pwd2: String,
397}
398
399impl PwdChange {
400 pub fn validate(&self) -> Result<(), RouteError> {
401 if self.old_pwd.is_empty() {
402 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
403 }
404 if self.new_pwd1.is_empty() || self.new_pwd2.is_empty() {
405 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
406 }
407 if self.new_pwd1 != self.new_pwd2 {
408 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
409 }
410 Ok(())
411 }
412}
413
414#[utoipa::path(
416 put,
417 path = "/me/password",
418 tag = "users",
419 request_body = PwdChange,
420 responses(
421 (status = 200, description = "Password changed successfully"),
422 (status = 400, description = "Invalid password or validation failed"),
423 (status = 401, description = "Not authenticated")
424 ),
425 security(("session_cookie" = []))
426)]
427pub async fn change_pwd(
428 user: MaybeUser,
429 State(db): DbState,
430 Json(pwd_change): Json<PwdChange>,
431) -> Result<(), RouteError> {
432 pwd_change.validate()?;
433
434 let Ok(user) = db.authenticate_user(user.name(), &pwd_change.old_pwd).await else {
435 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
436 };
437
438 db.change_pwd(&user.name, &pwd_change.new_pwd1).await?;
439 Ok(())
440}
441
442#[derive(Deserialize, ToSchema)]
443pub struct NewUser {
444 pub pwd1: String,
445 pub pwd2: String,
446 pub name: String,
447 #[serde(default)] pub is_admin: bool,
449 #[serde(default)] pub is_read_only: bool,
451}
452
453impl NewUser {
454 pub fn validate(&self) -> Result<(), RouteError> {
455 if self.name.is_empty() {
456 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
457 }
458 if self.pwd1.is_empty() || self.pwd2.is_empty() {
459 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
460 }
461 if self.pwd1 != self.pwd2 {
462 return Err(RouteError::Status(StatusCode::BAD_REQUEST));
463 }
464 Ok(())
465 }
466}
467
468#[utoipa::path(
470 post,
471 path = "/",
472 tag = "users",
473 request_body = NewUser,
474 responses(
475 (status = 200, description = "User created successfully"),
476 (status = 400, description = "Validation failed"),
477 (status = 403, description = "Admin access required")
478 ),
479 security(("session_cookie" = []))
480)]
481pub async fn add(
482 _user: AdminUser,
483 State(db): DbState,
484 State(cache): TokenCacheState,
485 Json(new_user): Json<NewUser>,
486) -> Result<(), RouteError> {
487 new_user.validate()?;
488
489 let salt = generate_salt();
490 db.add_user(
491 &new_user.name,
492 &new_user.pwd1,
493 &salt,
494 new_user.is_admin,
495 new_user.is_read_only,
496 )
497 .await?;
498
499 cache.invalidate_all();
500
501 Ok(())
502}
503
504#[cfg(test)]
505mod tests {
506 use std::sync::Arc;
507
508 use axum::Router;
509 use axum::body::Body;
510 use axum::routing::post;
511 use axum_extra::extract::cookie::Key;
512 use hyper::{Request, header};
513 use kellnr_appstate::AppStateData;
514 use kellnr_common::token_cache::{CachedTokenData, TokenCacheManager};
515 use kellnr_db::AuthToken;
516 use kellnr_db::error::DbError;
517 use kellnr_db::mock::MockDb;
518 use kellnr_settings::constants::COOKIE_SESSION_ID;
519 use kellnr_storage::cached_crate_storage::DynStorage;
520 use kellnr_storage::cratesio_crate_storage::CratesIoCrateStorage;
521 use kellnr_storage::fs_storage::FSStorage;
522 use kellnr_storage::kellnr_crate_storage::KellnrCrateStorage;
523 use mockall::predicate::*;
524 use tower::ServiceExt;
525
526 use super::*;
527 use crate::test_helper::{TEST_KEY, encode_cookies};
528
529 fn test_state_with_cache(mock_db: MockDb, cache: Arc<TokenCacheManager>) -> AppStateData {
530 let settings = Arc::new(kellnr_settings::test_settings());
531 let kellnr_storage =
532 Box::new(FSStorage::new(&settings.crates_path()).unwrap()) as DynStorage;
533 let crate_storage = Arc::new(KellnrCrateStorage::new(&settings, kellnr_storage));
534 let cratesio_storage = Arc::new(CratesIoCrateStorage::new(
535 &settings,
536 Box::new(FSStorage::new(&settings.crates_io_path()).unwrap()) as DynStorage,
537 ));
538 let (cratesio_prefetch_sender, _) = flume::unbounded();
539 let db: Arc<dyn kellnr_db::DbProvider> = Arc::new(mock_db);
540 let download_counter = Arc::new(kellnr_db::download_counter::DownloadCounter::new(
541 db.clone(),
542 30,
543 ));
544
545 AppStateData {
546 db,
547 signing_key: Key::from(TEST_KEY),
548 settings,
549 crate_storage,
550 cratesio_storage,
551 cratesio_prefetch_sender,
552 token_cache: cache,
553 toolchain_storage: None,
554 download_counter,
555 proxy_client: kellnr_common::cratesio_downloader::CLIENT.clone(),
556 }
557 }
558
559 #[tokio::test]
560 async fn test_add_token_invalidates_cache() {
561 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
563 cache
564 .insert(
565 "existing_token".to_string(),
566 CachedTokenData {
567 user: "test_user".to_string(),
568 is_admin: false,
569 is_read_only: false,
570 },
571 )
572 .await;
573
574 assert!(cache.get("existing_token").await.is_some());
576
577 let mut mock_db = MockDb::new();
578 mock_db
579 .expect_validate_session()
580 .times(1)
581 .returning(|_| Ok(("test_user".to_string(), false)));
582 mock_db
583 .expect_add_auth_token()
584 .times(1)
585 .returning(|_, _, _| Ok(()));
586
587 let state = test_state_with_cache(mock_db, cache.clone());
588 let app = Router::new()
589 .route("/add_token", post(add_token))
590 .with_state(state);
591
592 let response = app
593 .oneshot(
594 Request::post("/add_token")
595 .header(
596 header::COOKIE,
597 encode_cookies([(COOKIE_SESSION_ID, "session")]),
598 )
599 .header(header::CONTENT_TYPE, "application/json")
600 .body(Body::from(r#"{"name":"new_token"}"#))
601 .unwrap(),
602 )
603 .await
604 .unwrap();
605
606 assert!(
607 response.status().is_success(),
608 "Expected success but got {}",
609 response.status()
610 );
611
612 assert!(cache.get("existing_token").await.is_none());
614 }
615
616 #[tokio::test]
617 async fn test_delete_token_invalidates_cache() {
618 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
619 cache
620 .insert(
621 "token_to_keep".to_string(),
622 CachedTokenData {
623 user: "test_user".to_string(),
624 is_admin: false,
625 is_read_only: false,
626 },
627 )
628 .await;
629
630 assert!(cache.get("token_to_keep").await.is_some());
631
632 let mut mock_db = MockDb::new();
633 mock_db
634 .expect_validate_session()
635 .times(1)
636 .returning(|_| Ok(("test_user".to_string(), false)));
637 mock_db.expect_get_auth_tokens().times(1).returning(|_| {
638 Ok(vec![AuthToken::new(
639 1,
640 "token".to_string(),
641 "secret".to_string(),
642 )])
643 });
644 mock_db
645 .expect_delete_auth_token()
646 .times(1)
647 .with(eq(1))
648 .returning(|_| Ok(()));
649
650 let state = test_state_with_cache(mock_db, cache.clone());
651 let app = Router::new()
652 .route("/delete_token/{id}", axum::routing::delete(delete_token))
653 .with_state(state);
654
655 let response = app
656 .oneshot(
657 Request::delete("/delete_token/1")
658 .header(
659 header::COOKIE,
660 encode_cookies([(COOKIE_SESSION_ID, "session")]),
661 )
662 .body(Body::empty())
663 .unwrap(),
664 )
665 .await
666 .unwrap();
667
668 assert!(
669 response.status().is_success(),
670 "Expected success but got {}",
671 response.status()
672 );
673
674 assert!(cache.get("token_to_keep").await.is_none());
676 }
677
678 #[tokio::test]
679 async fn test_delete_user_invalidates_cache() {
680 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
681 cache
682 .insert(
683 "user_token".to_string(),
684 CachedTokenData {
685 user: "user_to_delete".to_string(),
686 is_admin: false,
687 is_read_only: false,
688 },
689 )
690 .await;
691
692 assert!(cache.get("user_token").await.is_some());
693
694 let mut mock_db = MockDb::new();
695 mock_db
696 .expect_validate_session()
697 .times(1)
698 .returning(|_| Ok(("admin".to_string(), true))); mock_db
700 .expect_delete_user()
701 .times(1)
702 .with(eq("user_to_delete"))
703 .returning(|_| Ok(()));
704
705 let state = test_state_with_cache(mock_db, cache.clone());
706 let app = Router::new()
707 .route("/delete/{name}", axum::routing::delete(delete))
708 .with_state(state);
709
710 let response = app
711 .oneshot(
712 Request::delete("/delete/user_to_delete")
713 .header(
714 header::COOKIE,
715 encode_cookies([(COOKIE_SESSION_ID, "session")]),
716 )
717 .body(Body::empty())
718 .unwrap(),
719 )
720 .await
721 .unwrap();
722
723 assert!(
724 response.status().is_success(),
725 "Expected success but got {}",
726 response.status()
727 );
728
729 assert!(cache.get("user_token").await.is_none());
731 }
732
733 #[tokio::test]
734 async fn test_read_only_change_invalidates_cache() {
735 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
736 cache
737 .insert(
738 "user_token".to_string(),
739 CachedTokenData {
740 user: "target_user".to_string(),
741 is_admin: false,
742 is_read_only: false, },
744 )
745 .await;
746
747 assert!(cache.get("user_token").await.is_some());
748
749 let mut mock_db = MockDb::new();
750 mock_db
751 .expect_validate_session()
752 .times(1)
753 .returning(|_| Ok(("admin".to_string(), true))); mock_db
755 .expect_change_read_only_state()
756 .times(1)
757 .with(eq("target_user"), eq(true))
758 .returning(|_, _| Ok(()));
759
760 let state = test_state_with_cache(mock_db, cache.clone());
761 let app = Router::new()
762 .route("/read_only/{name}", post(read_only))
763 .with_state(state);
764
765 let response = app
766 .oneshot(
767 Request::post("/read_only/target_user")
768 .header(
769 header::COOKIE,
770 encode_cookies([(COOKIE_SESSION_ID, "session")]),
771 )
772 .header(header::CONTENT_TYPE, "application/json")
773 .body(Body::from(r#"{"state":true}"#))
774 .unwrap(),
775 )
776 .await
777 .unwrap();
778
779 assert!(
780 response.status().is_success(),
781 "Expected success but got {}",
782 response.status()
783 );
784
785 assert!(cache.get("user_token").await.is_none());
787 }
788
789 #[tokio::test]
790 async fn test_admin_self_locking_prevented() {
791 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
792
793 let mut mock_db = MockDb::new();
794 mock_db
795 .expect_validate_session()
796 .times(1)
797 .returning(|_| Ok(("admin".to_string(), true))); let state = test_state_with_cache(mock_db, cache.clone());
801 let app = Router::new()
802 .route("/read_only/{name}", post(read_only))
803 .with_state(state);
804
805 let response = app
806 .oneshot(
807 Request::post("/read_only/admin") .header(
809 header::COOKIE,
810 encode_cookies([(COOKIE_SESSION_ID, "session")]),
811 )
812 .header(header::CONTENT_TYPE, "application/json")
813 .body(Body::from(r#"{"state":true}"#)) .unwrap(),
815 )
816 .await
817 .unwrap();
818
819 assert_eq!(
821 response.status(),
822 StatusCode::BAD_REQUEST,
823 "Expected BAD_REQUEST but got {}",
824 response.status()
825 );
826 }
827
828 #[tokio::test]
829 async fn test_admin_self_unlocking_allowed() {
830 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
833
834 let mut mock_db = MockDb::new();
835 mock_db
836 .expect_validate_session()
837 .times(1)
838 .returning(|_| Ok(("admin".to_string(), true))); mock_db
840 .expect_change_read_only_state()
841 .times(1)
842 .with(eq("admin"), eq(false)) .returning(|_, _| Ok(()));
844
845 let state = test_state_with_cache(mock_db, cache.clone());
846 let app = Router::new()
847 .route("/read_only/{name}", post(read_only))
848 .with_state(state);
849
850 let response = app
851 .oneshot(
852 Request::post("/read_only/admin") .header(
854 header::COOKIE,
855 encode_cookies([(COOKIE_SESSION_ID, "session")]),
856 )
857 .header(header::CONTENT_TYPE, "application/json")
858 .body(Body::from(r#"{"state":false}"#)) .unwrap(),
860 )
861 .await
862 .unwrap();
863
864 assert!(
865 response.status().is_success(),
866 "Expected success but got {}",
867 response.status()
868 );
869 }
870
871 #[tokio::test]
872 async fn test_admin_locking_other_user_works() {
873 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
874
875 let mut mock_db = MockDb::new();
876 mock_db
877 .expect_validate_session()
878 .times(1)
879 .returning(|_| Ok(("admin".to_string(), true))); mock_db
881 .expect_change_read_only_state()
882 .times(1)
883 .with(eq("other_user"), eq(true))
884 .returning(|_, _| Ok(()));
885
886 let state = test_state_with_cache(mock_db, cache.clone());
887 let app = Router::new()
888 .route("/read_only/{name}", post(read_only))
889 .with_state(state);
890
891 let response = app
892 .oneshot(
893 Request::post("/read_only/other_user") .header(
895 header::COOKIE,
896 encode_cookies([(COOKIE_SESSION_ID, "session")]),
897 )
898 .header(header::CONTENT_TYPE, "application/json")
899 .body(Body::from(r#"{"state":true}"#))
900 .unwrap(),
901 )
902 .await
903 .unwrap();
904
905 assert!(
906 response.status().is_success(),
907 "Expected success but got {}",
908 response.status()
909 );
910 }
911
912 #[tokio::test]
913 async fn test_add_user_invalidates_cache() {
914 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
915 cache
916 .insert(
917 "existing_token".to_string(),
918 CachedTokenData {
919 user: "existing_user".to_string(),
920 is_admin: false,
921 is_read_only: false,
922 },
923 )
924 .await;
925
926 assert!(cache.get("existing_token").await.is_some());
927
928 let mut mock_db = MockDb::new();
929 mock_db
930 .expect_validate_session()
931 .times(1)
932 .returning(|_| Ok(("admin".to_string(), true))); mock_db
934 .expect_add_user()
935 .times(1)
936 .returning(|_, _, _, _, _| Ok(()));
937
938 let state = test_state_with_cache(mock_db, cache.clone());
939 let app = Router::new().route("/add", post(add)).with_state(state);
940
941 let response = app
942 .oneshot(
943 Request::post("/add")
944 .header(header::COOKIE, encode_cookies([(COOKIE_SESSION_ID, "session")]))
945 .header(header::CONTENT_TYPE, "application/json")
946 .body(Body::from(r#"{"name":"new_user","pwd1":"password","pwd2":"password","is_admin":false,"is_read_only":false}"#))
947 .unwrap(),
948 )
949 .await
950 .unwrap();
951
952 assert!(
953 response.status().is_success(),
954 "Expected success but got {}",
955 response.status()
956 );
957
958 assert!(cache.get("existing_token").await.is_none());
960 }
961
962 #[tokio::test]
963 async fn test_cache_not_invalidated_on_db_failure() {
964 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
966 cache
967 .insert(
968 "existing_token".to_string(),
969 CachedTokenData {
970 user: "test_user".to_string(),
971 is_admin: false,
972 is_read_only: false,
973 },
974 )
975 .await;
976
977 assert!(cache.get("existing_token").await.is_some());
978
979 let mut mock_db = MockDb::new();
980 mock_db
981 .expect_validate_session()
982 .times(1)
983 .returning(|_| Ok(("test_user".to_string(), false)));
984 mock_db
985 .expect_add_auth_token()
986 .times(1)
987 .returning(|_, _, _| {
988 Err(DbError::InitializationError(
989 "Connection timeout".to_string(),
990 ))
991 });
992
993 let state = test_state_with_cache(mock_db, cache.clone());
994 let app = Router::new()
995 .route("/add_token", post(add_token))
996 .with_state(state);
997
998 let response = app
999 .oneshot(
1000 Request::post("/add_token")
1001 .header(
1002 header::COOKIE,
1003 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1004 )
1005 .header(header::CONTENT_TYPE, "application/json")
1006 .body(Body::from(r#"{"name":"new_token"}"#))
1007 .unwrap(),
1008 )
1009 .await
1010 .unwrap();
1011
1012 assert!(!response.status().is_success());
1014
1015 assert!(cache.get("existing_token").await.is_some());
1017 }
1018
1019 #[tokio::test]
1020 async fn test_admin_change_invalidates_cache() {
1021 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1022 cache
1023 .insert(
1024 "user_token".to_string(),
1025 CachedTokenData {
1026 user: "target_user".to_string(),
1027 is_admin: false, is_read_only: false,
1029 },
1030 )
1031 .await;
1032
1033 assert!(cache.get("user_token").await.is_some());
1034
1035 let mut mock_db = MockDb::new();
1036 mock_db
1037 .expect_validate_session()
1038 .times(1)
1039 .returning(|_| Ok(("admin".to_string(), true))); mock_db
1041 .expect_change_admin_state()
1042 .times(1)
1043 .with(eq("target_user"), eq(true))
1044 .returning(|_, _| Ok(()));
1045
1046 let state = test_state_with_cache(mock_db, cache.clone());
1047 let app = Router::new()
1048 .route("/admin/{name}", post(admin))
1049 .with_state(state);
1050
1051 let response = app
1052 .oneshot(
1053 Request::post("/admin/target_user")
1054 .header(
1055 header::COOKIE,
1056 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1057 )
1058 .header(header::CONTENT_TYPE, "application/json")
1059 .body(Body::from(r#"{"state":true}"#))
1060 .unwrap(),
1061 )
1062 .await
1063 .unwrap();
1064
1065 assert!(
1066 response.status().is_success(),
1067 "Expected success but got {}",
1068 response.status()
1069 );
1070
1071 assert!(cache.get("user_token").await.is_none());
1073 }
1074
1075 #[tokio::test]
1076 async fn test_admin_self_demotion_prevented() {
1077 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1078
1079 let mut mock_db = MockDb::new();
1080 mock_db
1081 .expect_validate_session()
1082 .times(1)
1083 .returning(|_| Ok(("admin".to_string(), true))); let state = test_state_with_cache(mock_db, cache.clone());
1087 let app = Router::new()
1088 .route("/admin/{name}", post(admin))
1089 .with_state(state);
1090
1091 let response = app
1092 .oneshot(
1093 Request::post("/admin/admin") .header(
1095 header::COOKIE,
1096 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1097 )
1098 .header(header::CONTENT_TYPE, "application/json")
1099 .body(Body::from(r#"{"state":false}"#)) .unwrap(),
1101 )
1102 .await
1103 .unwrap();
1104
1105 assert_eq!(
1107 response.status(),
1108 StatusCode::BAD_REQUEST,
1109 "Expected BAD_REQUEST but got {}",
1110 response.status()
1111 );
1112 }
1113
1114 #[tokio::test]
1115 async fn test_non_admin_cannot_change_admin_status() {
1116 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1117
1118 let mut mock_db = MockDb::new();
1119 mock_db
1120 .expect_validate_session()
1121 .times(1)
1122 .returning(|_| Ok(("regular_user".to_string(), false))); let state = test_state_with_cache(mock_db, cache.clone());
1125 let app = Router::new()
1126 .route("/admin/{name}", post(admin))
1127 .with_state(state);
1128
1129 let response = app
1130 .oneshot(
1131 Request::post("/admin/target_user")
1132 .header(
1133 header::COOKIE,
1134 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1135 )
1136 .header(header::CONTENT_TYPE, "application/json")
1137 .body(Body::from(r#"{"state":true}"#))
1138 .unwrap(),
1139 )
1140 .await
1141 .unwrap();
1142
1143 assert_eq!(
1145 response.status(),
1146 StatusCode::FORBIDDEN,
1147 "Expected FORBIDDEN but got {}",
1148 response.status()
1149 );
1150 }
1151
1152 #[tokio::test]
1153 async fn test_admin_demotion_works() {
1154 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1155
1156 let mut mock_db = MockDb::new();
1157 mock_db
1158 .expect_validate_session()
1159 .times(1)
1160 .returning(|_| Ok(("admin".to_string(), true))); mock_db
1162 .expect_change_admin_state()
1163 .times(1)
1164 .with(eq("other_admin"), eq(false)) .returning(|_, _| Ok(()));
1166
1167 let state = test_state_with_cache(mock_db, cache.clone());
1168 let app = Router::new()
1169 .route("/admin/{name}", post(admin))
1170 .with_state(state);
1171
1172 let response = app
1173 .oneshot(
1174 Request::post("/admin/other_admin")
1175 .header(
1176 header::COOKIE,
1177 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1178 )
1179 .header(header::CONTENT_TYPE, "application/json")
1180 .body(Body::from(r#"{"state":false}"#)) .unwrap(),
1182 )
1183 .await
1184 .unwrap();
1185
1186 assert!(
1187 response.status().is_success(),
1188 "Expected success but got {}",
1189 response.status()
1190 );
1191 }
1192
1193 #[tokio::test]
1194 async fn test_admin_nonexistent_user_returns_error() {
1195 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1196
1197 let mut mock_db = MockDb::new();
1198 mock_db
1199 .expect_validate_session()
1200 .times(1)
1201 .returning(|_| Ok(("admin".to_string(), true))); mock_db
1203 .expect_change_admin_state()
1204 .times(1)
1205 .with(eq("nonexistent"), eq(true))
1206 .returning(|_, _| Err(DbError::UserNotFound("nonexistent".to_string())));
1207
1208 let state = test_state_with_cache(mock_db, cache.clone());
1209 let app = Router::new()
1210 .route("/admin/{name}", post(admin))
1211 .with_state(state);
1212
1213 let response = app
1214 .oneshot(
1215 Request::post("/admin/nonexistent")
1216 .header(
1217 header::COOKIE,
1218 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1219 )
1220 .header(header::CONTENT_TYPE, "application/json")
1221 .body(Body::from(r#"{"state":true}"#))
1222 .unwrap(),
1223 )
1224 .await
1225 .unwrap();
1226
1227 assert_eq!(
1229 response.status(),
1230 StatusCode::NOT_FOUND,
1231 "Expected NOT_FOUND but got {}",
1232 response.status()
1233 );
1234 }
1235
1236 #[tokio::test]
1237 async fn test_admin_self_promotion_allowed() {
1238 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1241
1242 let mut mock_db = MockDb::new();
1243 mock_db
1244 .expect_validate_session()
1245 .times(1)
1246 .returning(|_| Ok(("admin".to_string(), true))); mock_db
1248 .expect_change_admin_state()
1249 .times(1)
1250 .with(eq("admin"), eq(true)) .returning(|_, _| Ok(()));
1252
1253 let state = test_state_with_cache(mock_db, cache.clone());
1254 let app = Router::new()
1255 .route("/admin/{name}", post(admin))
1256 .with_state(state);
1257
1258 let response = app
1259 .oneshot(
1260 Request::post("/admin/admin") .header(
1262 header::COOKIE,
1263 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1264 )
1265 .header(header::CONTENT_TYPE, "application/json")
1266 .body(Body::from(r#"{"state":true}"#)) .unwrap(),
1268 )
1269 .await
1270 .unwrap();
1271
1272 assert!(
1273 response.status().is_success(),
1274 "Expected success but got {}",
1275 response.status()
1276 );
1277 }
1278
1279 #[tokio::test]
1280 async fn test_admin_self_deletion_prevented() {
1281 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1282
1283 let mut mock_db = MockDb::new();
1284 mock_db
1285 .expect_validate_session()
1286 .times(1)
1287 .returning(|_| Ok(("admin".to_string(), true))); let state = test_state_with_cache(mock_db, cache.clone());
1291 let app = Router::new()
1292 .route("/delete/{name}", axum::routing::delete(delete))
1293 .with_state(state);
1294
1295 let response = app
1296 .oneshot(
1297 Request::delete("/delete/admin") .header(
1299 header::COOKIE,
1300 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1301 )
1302 .body(Body::empty())
1303 .unwrap(),
1304 )
1305 .await
1306 .unwrap();
1307
1308 assert_eq!(
1310 response.status(),
1311 StatusCode::BAD_REQUEST,
1312 "Expected BAD_REQUEST but got {}",
1313 response.status()
1314 );
1315 }
1316
1317 #[tokio::test]
1318 async fn test_admin_deletion_of_other_user_works() {
1319 let cache = Arc::new(TokenCacheManager::new(true, 60, 100));
1320
1321 let mut mock_db = MockDb::new();
1322 mock_db
1323 .expect_validate_session()
1324 .times(1)
1325 .returning(|_| Ok(("admin".to_string(), true))); mock_db
1327 .expect_delete_user()
1328 .times(1)
1329 .with(eq("other_user"))
1330 .returning(|_| Ok(()));
1331
1332 let state = test_state_with_cache(mock_db, cache.clone());
1333 let app = Router::new()
1334 .route("/delete/{name}", axum::routing::delete(delete))
1335 .with_state(state);
1336
1337 let response = app
1338 .oneshot(
1339 Request::delete("/delete/other_user") .header(
1341 header::COOKIE,
1342 encode_cookies([(COOKIE_SESSION_ID, "session")]),
1343 )
1344 .body(Body::empty())
1345 .unwrap(),
1346 )
1347 .await
1348 .unwrap();
1349
1350 assert!(
1351 response.status().is_success(),
1352 "Expected success but got {}",
1353 response.status()
1354 );
1355 }
1356}