diff --git a/docker/Dockerfile b/Dockerfile similarity index 97% rename from docker/Dockerfile rename to Dockerfile index fc204fe1..c68bcf32 100644 --- a/docker/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app RUN apt update && apt install lld clang -y FROM chef as planner -COPY .. . +COPY . . # Compute a lock-like file for our project RUN cargo chef prepare --recipe-path recipe.json @@ -12,7 +12,7 @@ FROM chef as builder COPY --from=planner /app/recipe.json recipe.json # Build our project dependencies RUN cargo chef cook --release --recipe-path recipe.json -COPY .. . +COPY . . ENV SQLX_OFFLINE true # Build the project RUN cargo build --release --bin appflowy_cloud diff --git a/docker-compose.yml b/docker-compose.yml index 642fcff2..073b2b75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,7 +55,7 @@ services: - APP__GOTRUE__JWT_SECRET=${GOTRUE_JWT_SECRET} build: context: . - dockerfile: docker/Dockerfile + dockerfile: Dockerfile image: appflowy_cloud:${BACKEND_VERSION:-latest} depends_on: - redis diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 623a9ee5..cb2e2530 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use gotrue_entity::OAuthProvider; use gotrue_entity::OAuthURL; use reqwest::Method; @@ -112,10 +114,11 @@ impl Client { Ok(()) } - pub async fn profile(&self) -> Result { + pub async fn profile(&mut self) -> Result { let url = format!("{}/api/user/profile", self.base_url); let resp = self - .http_client_with_auth(Method::GET, &url)? + .http_client_with_auth(Method::GET, &url) + .await? .send() .await?; AppResponse::::from_response(resp) @@ -123,10 +126,11 @@ impl Client { .into_data() } - pub async fn workspaces(&self) -> Result { + pub async fn workspaces(&mut self) -> Result { let url = format!("{}/api/user/workspaces", self.base_url); let resp = self - .http_client_with_auth(Method::GET, &url)? + .http_client_with_auth(Method::GET, &url) + .await? .send() .await?; AppResponse::::from_response(resp) @@ -169,10 +173,11 @@ impl Client { Ok(()) } - pub async fn sign_out(&self) -> Result<(), AppError> { + pub async fn sign_out(&mut self) -> Result<(), AppError> { let url = format!("{}/api/user/sign_out", self.base_url); let resp = self - .http_client_with_auth(Method::POST, &url)? + .http_client_with_auth(Method::POST, &url) + .await? .send() .await?; AppResponse::<()>::from_response(resp).await?.into_error()?; @@ -186,7 +191,8 @@ impl Client { "password": password, }); let resp = self - .http_client_with_auth(Method::POST, &url)? + .http_client_with_auth(Method::POST, &url) + .await? .json(&payload) .send() .await?; @@ -199,30 +205,33 @@ impl Client { Ok(()) } - pub async fn create_collab(&self, params: InsertCollabParams) -> Result<(), AppError> { + pub async fn create_collab(&mut self, params: InsertCollabParams) -> Result<(), AppError> { let url = format!("{}/api/collab/", self.base_url); let resp = self - .http_client_with_auth(Method::POST, &url)? + .http_client_with_auth(Method::POST, &url) + .await? .json(¶ms) .send() .await?; AppResponse::<()>::from_response(resp).await?.into_error() } - pub async fn update_collab(&self, params: InsertCollabParams) -> Result<(), AppError> { + pub async fn update_collab(&mut self, params: InsertCollabParams) -> Result<(), AppError> { let url = format!("{}/api/collab/", self.base_url); let resp = self - .http_client_with_auth(Method::PUT, &url)? + .http_client_with_auth(Method::PUT, &url) + .await? .json(¶ms) .send() .await?; AppResponse::<()>::from_response(resp).await?.into_error() } - pub async fn get_collab(&self, params: QueryCollabParams) -> Result { + pub async fn get_collab(&mut self, params: QueryCollabParams) -> Result { let url = format!("{}/api/collab/", self.base_url); let resp = self - .http_client_with_auth(Method::GET, &url)? + .http_client_with_auth(Method::GET, &url) + .await? .json(¶ms) .send() .await?; @@ -231,10 +240,11 @@ impl Client { .into_data() } - pub async fn delete_collab(&self, params: DeleteCollabParams) -> Result<(), AppError> { + pub async fn delete_collab(&mut self, params: DeleteCollabParams) -> Result<(), AppError> { let url = format!("{}/api/collab/", self.base_url); let resp = self - .http_client_with_auth(Method::DELETE, &url)? + .http_client_with_auth(Method::DELETE, &url) + .await? .json(¶ms) .send() .await?; @@ -254,17 +264,34 @@ impl Client { } } - fn http_client_with_auth(&self, method: Method, url: &str) -> Result { - match &self.token { - None => Err(ErrorCode::NotLoggedIn.into()), - Some(t) => { - let request_builder = self - .http_client - .request(method, url) - .bearer_auth(&t.access_token); - Ok(request_builder) - }, + async fn http_client_with_auth( + &mut self, + method: Method, + url: &str, + ) -> Result { + let token = self.token().ok_or(ErrorCode::NotLoggedIn)?; + + // Refresh token if it's about to expire + let time_now_sec = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + if time_now_sec + 60 > token.expires_at { + // Add 60 seconds buffer + self.refresh().await?; } + + let access_token = self + .token() + .ok_or(ErrorCode::NotLoggedIn)? + .access_token + .as_str(); + + let request_builder = self + .http_client + .request(method, url) + .bearer_auth(access_token); + Ok(request_builder) } // pub async fn change_password( diff --git a/libs/shared-entity/src/error.rs b/libs/shared-entity/src/error.rs index 5225a217..5d34c20f 100644 --- a/libs/shared-entity/src/error.rs +++ b/libs/shared-entity/src/error.rs @@ -1,5 +1,6 @@ use std::fmt::Display; use std::num::ParseIntError; +use std::time::SystemTimeError; use std::{borrow::Cow, str}; use serde::{Deserialize, Serialize}; @@ -133,3 +134,9 @@ impl From for AppError { AppError::new(ErrorCode::InvalidUrl, value.to_string()) } } + +impl From for AppError { + fn from(value: SystemTimeError) -> Self { + AppError::new(ErrorCode::Unhandled, value.to_string()) + } +} diff --git a/tests/client/refresh.rs b/tests/client/refresh.rs index 4e9db6bb..9120b01b 100644 --- a/tests/client/refresh.rs +++ b/tests/client/refresh.rs @@ -1,3 +1,7 @@ +use std::time::SystemTime; + +use gotrue_entity::AccessTokenResponse; + use crate::{ client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX}, client_api_client, @@ -11,5 +15,39 @@ async fn refresh_success() { let password = ®ISTERED_PASSWORD; let mut c = client_api_client(); c.sign_in_password(email, password).await.unwrap(); + let old_token = c.token().unwrap().access_token.to_owned(); + std::thread::sleep(std::time::Duration::from_secs(2)); c.refresh().await.unwrap(); + let new_token = c.token().unwrap().access_token.to_owned(); + assert_ne!(old_token, new_token); +} + +#[tokio::test] +async fn refresh_trigger() { + let _guard = REGISTERED_USER_MUTEX.lock().await; + + let email = ®ISTERED_EMAIL; + let password = ®ISTERED_PASSWORD; + + let mut c = client_api_client(); + + c.sign_in_password(email, password).await.unwrap(); + std::thread::sleep(std::time::Duration::from_secs(2)); + let token = c.token().unwrap(); + let old_access_token = token.access_token.to_owned(); + + // Set the token to be expired + unsafe { + let token_mut = token as *const AccessTokenResponse as *mut AccessTokenResponse; + token_mut.as_mut().unwrap().expires_at = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() as i64 + }; + + // querying that requires auth should trigger a refresh + let _workspaces = c.workspaces().await.unwrap(); + let new_token = c.token().unwrap().access_token.to_owned(); + + assert_ne!(old_access_token, new_token); } diff --git a/tests/client/sign_out.rs b/tests/client/sign_out.rs index 8b8709a3..f7a356b4 100644 --- a/tests/client/sign_out.rs +++ b/tests/client/sign_out.rs @@ -3,7 +3,7 @@ use crate::client_api_client; #[tokio::test] async fn sign_out_but_not_sign_in() { - let c = client_api_client(); + let mut c = client_api_client(); let res = c.sign_out().await; assert!(res.is_err()); } diff --git a/tests/collab/mod.rs b/tests/collab/mod.rs index fe2b799d..aaa6e2a4 100644 --- a/tests/collab/mod.rs +++ b/tests/collab/mod.rs @@ -2,7 +2,7 @@ use client_api::Client; mod storage_test; -pub(crate) async fn workspace_id_from_client(c: &Client) -> String { +pub(crate) async fn workspace_id_from_client(c: &mut Client) -> String { c.workspaces() .await .unwrap() diff --git a/tests/collab/storage_test.rs b/tests/collab/storage_test.rs index 1f43d773..b86a93a3 100644 --- a/tests/collab/storage_test.rs +++ b/tests/collab/storage_test.rs @@ -17,7 +17,7 @@ async fn success_insert_collab_test() { .unwrap(); let raw_data = "hello world".to_string().as_bytes().to_vec(); - let workspace_id = workspace_id_from_client(&c).await; + let workspace_id = workspace_id_from_client(&mut c).await; let object_id = Uuid::new_v4().to_string(); c.create_collab(InsertCollabParams::new( 1, @@ -50,7 +50,7 @@ async fn success_delete_collab_test() { .unwrap(); let raw_data = "hello world".to_string().as_bytes().to_vec(); - let workspace_id = workspace_id_from_client(&c).await; + let workspace_id = workspace_id_from_client(&mut c).await; let object_id = Uuid::new_v4().to_string(); c.create_collab(InsertCollabParams::new( 1, @@ -88,7 +88,7 @@ async fn fail_insert_collab_with_empty_payload_test() { .await .unwrap(); - let workspace_id = workspace_id_from_client(&c).await; + let workspace_id = workspace_id_from_client(&mut c).await; let error = c .create_collab(InsertCollabParams::new( 1, diff --git a/tests/realtime/edit_collab_test.rs b/tests/realtime/edit_collab_test.rs index b90a3ef4..32329abf 100644 --- a/tests/realtime/edit_collab_test.rs +++ b/tests/realtime/edit_collab_test.rs @@ -13,7 +13,7 @@ use storage::collab::FLUSH_PER_UPDATE; async fn realtime_write_collab_test() { let object_id = uuid::Uuid::new_v4().to_string(); let collab_type = CollabType::Document; - let test_client = TestClient::new(&object_id, collab_type.clone()).await; + let mut test_client = TestClient::new(&object_id, collab_type.clone()).await; // Edit the collab for i in 0..=5 { @@ -28,7 +28,7 @@ async fn realtime_write_collab_test() { test_client.disconnect().await; assert_collab_json( - &test_client.api_client, + &mut test_client.api_client, &object_id, &collab_type, 3, @@ -48,7 +48,7 @@ async fn realtime_write_collab_test() { async fn one_direction_peer_sync_test() { let object_id = uuid::Uuid::new_v4().to_string(); let collab_type = CollabType::Document; - let client_1 = TestClient::new(&object_id, collab_type.clone()).await; + let mut client_1 = TestClient::new(&object_id, collab_type.clone()).await; let client_2 = TestClient::new(&object_id, collab_type.clone()).await; // Edit the collab from client 1 and then the server will broadcast to client 2 @@ -58,7 +58,7 @@ async fn one_direction_peer_sync_test() { } assert_collab_json( - &client_1.api_client, + &mut client_1.api_client, &object_id, &collab_type, 5, @@ -129,13 +129,13 @@ async fn multiple_collab_edit_test() { let collab_type = CollabType::Document; let object_id_1 = uuid::Uuid::new_v4().to_string(); - let client_1 = TestClient::new(&object_id_1, collab_type.clone()).await; + let mut client_1 = TestClient::new(&object_id_1, collab_type.clone()).await; let object_id_2 = uuid::Uuid::new_v4().to_string(); - let client_2 = TestClient::new(&object_id_2, collab_type.clone()).await; + let mut client_2 = TestClient::new(&object_id_2, collab_type.clone()).await; let object_id_3 = uuid::Uuid::new_v4().to_string(); - let client_3 = TestClient::new(&object_id_3, collab_type.clone()).await; + let mut client_3 = TestClient::new(&object_id_3, collab_type.clone()).await; client_1.collab.lock().insert("title", "I am client 1"); client_2.collab.lock().insert("title", "I am client 2"); @@ -143,7 +143,7 @@ async fn multiple_collab_edit_test() { tokio::time::sleep(Duration::from_secs(2)).await; assert_collab_json( - &client_1.api_client, + &mut client_1.api_client, &object_id_1, &collab_type, 3, @@ -154,7 +154,7 @@ async fn multiple_collab_edit_test() { .await; assert_collab_json( - &client_2.api_client, + &mut client_2.api_client, &object_id_2, &collab_type, 3, @@ -164,7 +164,7 @@ async fn multiple_collab_edit_test() { ) .await; assert_collab_json( - &client_3.api_client, + &mut client_3.api_client, &object_id_3, &collab_type, 3, diff --git a/tests/realtime/test_client.rs b/tests/realtime/test_client.rs index 8674a320..711158f4 100644 --- a/tests/realtime/test_client.rs +++ b/tests/realtime/test_client.rs @@ -94,7 +94,7 @@ impl TestClient { #[allow(dead_code)] pub async fn assert_collab_json( - client: &client_api::Client, + client: &mut client_api::Client, object_id: &str, collab_type: &CollabType, secs: u64, @@ -141,7 +141,7 @@ pub async fn assert_collab_json( #[allow(dead_code)] pub async fn get_collab_json_from_server( - client: &client_api::Client, + client: &mut client_api::Client, object_id: &str, collab_type: CollabType, ) -> serde_json::Value {