Sfoglia il codice sorgente

fix: refresh login token on repeated login

LoganZ2 3 ore fa
parent
commit
592d1821c6
4 ha cambiato i file con 77 aggiunte e 8 eliminazioni
  1. 3 0
      .gitignore
  2. 28 0
      src/db.rs
  3. 15 7
      src/service.rs
  4. 31 1
      tests/core_flow.rs

+ 3 - 0
.gitignore

@@ -1,3 +1,6 @@
 /target
 /reference/
 /private/
+/frontend/node_modules/
+/frontend/dist/
+/frontend/tsconfig.tsbuildinfo

+ 28 - 0
src/db.rs

@@ -572,6 +572,34 @@ impl Database {
         })
     }
 
+    pub fn replace_access_token(
+        &self,
+        user_id: i64,
+        name: &str,
+        token_hash: &str,
+    ) -> AppResult<AccessToken> {
+        let conn = self.lock()?;
+        let tx = conn.unchecked_transaction()?;
+        let now = now_unix();
+        tx.execute(
+            "DELETE FROM access_token WHERE user_id = ?1 AND name = ?2",
+            params![user_id, name],
+        )?;
+        tx.execute(
+            r#"
+            INSERT INTO access_token (user_id, name, token_hash, created_unix, updated_unix)
+            VALUES (?1, ?2, ?3, ?4, 0)
+            "#,
+            params![user_id, name, token_hash, now],
+        )?;
+        let id = tx.last_insert_rowid();
+        tx.commit()?;
+        drop(conn);
+        self.get_access_token_by_id(id)?.ok_or_else(|| {
+            AppError::NotFound(format!("access token disappeared after replace: {id}"))
+        })
+    }
+
     pub fn get_access_token_by_id(&self, id: i64) -> AppResult<Option<AccessToken>> {
         let conn = self.lock()?;
         let mut stmt = conn.prepare(

+ 15 - 7
src/service.rs

@@ -81,13 +81,7 @@ pub fn login(state: &AppState, req: LoginRequest) -> AppResult<LoginResponse> {
         .verify_password(req.password.as_bytes(), &parsed_hash)
         .map_err(|_| AppError::Unauthorized("invalid credentials".to_string()))?;
 
-    let token = issue_access_token(
-        state,
-        user.id,
-        CreateAccessTokenRequest {
-            name: "login".to_string(),
-        },
-    )?;
+    let token = issue_login_access_token(state, user.id)?;
 
     Ok(LoginResponse {
         token: token.token,
@@ -95,6 +89,20 @@ pub fn login(state: &AppState, req: LoginRequest) -> AppResult<LoginResponse> {
     })
 }
 
+fn issue_login_access_token(state: &AppState, user_id: i64) -> AppResult<CreateAccessTokenResponse> {
+    let token = random_token();
+    let token_hash = hash_token(&token);
+    let record = state.db.replace_access_token(user_id, "login", &token_hash)?;
+
+    Ok(CreateAccessTokenResponse {
+        id: record.id,
+        name: record.name,
+        token,
+        created_unix: record.created_unix,
+        updated_unix: record.updated_unix,
+    })
+}
+
 pub fn issue_access_token(
     state: &AppState,
     user_id: i64,

+ 31 - 1
tests/core_flow.rs

@@ -22,7 +22,7 @@ use gitr::{
     models::{
         AccessMode, ApiCollaboratorResponse, ApiLoginResponse, ApiPullRequestDetailResponse,
         ApiPullRequestResponse, ApiRepositoryResponse, ApiUser, Branch, CompareResponse,
-        CreateAccessTokenResponse, PullRequestStatus,
+        CreateAccessTokenResponse, AccessTokenResponse, PullRequestStatus,
     },
 };
 use serde_json::Value;
@@ -228,6 +228,36 @@ async fn login_rejects_bad_password() {
     assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
+#[actix_web::test]
+async fn login_replaces_existing_login_token() {
+    let env = TestEnv::new("repeat-login");
+    let app = env.app().await;
+
+    create_user(&app, "grace").await;
+
+    let first = login(&app, "grace").await;
+    let second = login(&app, "grace").await;
+
+    assert_ne!(first.token, second.token);
+
+    let first_request = test::TestRequest::get()
+        .uri("/api/user/tokens")
+        .insert_header(("authorization", format!("Bearer {}", first.token)))
+        .to_request();
+    let first_response = test::call_service(&app, first_request).await;
+    assert_eq!(first_response.status(), StatusCode::UNAUTHORIZED);
+
+    let second_request = test::TestRequest::get()
+        .uri("/api/user/tokens")
+        .insert_header(("authorization", format!("Bearer {}", second.token)))
+        .to_request();
+    let second_response = test::call_service(&app, second_request).await;
+    assert_eq!(second_response.status(), StatusCode::OK);
+    let tokens: Vec<AccessTokenResponse> = test::read_body_json(second_response).await;
+    assert_eq!(tokens.len(), 1);
+    assert_eq!(tokens[0].name, "login");
+}
+
 #[actix_web::test]
 async fn token_endpoint_creates_second_token() {
     let env = TestEnv::new("token-endpoint");