feat: added refresh ability on server and client (#41)

* feat: added refresh ability on server and client

* fix: use refresh token for refresh and add test case

* chore: cargo fmt --all

* chore: cargo clippy

* fix: cargo clippy

* test: added async mutex for registered user for consistency

* fix: remove unneeded files

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Zack 2023-09-14 15:58:18 +08:00 committed by GitHub
parent e03a6ce587
commit b3be09e264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 107 additions and 19 deletions

View File

@ -1,4 +1,3 @@
use anyhow::Error;
use gotrue_entity::OAuthProvider; use gotrue_entity::OAuthProvider;
use gotrue_entity::OAuthURL; use gotrue_entity::OAuthURL;
use reqwest::Method; use reqwest::Method;
@ -146,6 +145,19 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn refresh(&mut self) -> Result<(), AppError> {
let refresh_token = self
.token
.as_ref()
.ok_or::<AppError>(ErrorCode::NotLoggedIn.into())?
.refresh_token
.as_str();
let url = format!("{}/api/user/refresh/{}", self.base_url, refresh_token);
let resp = self.http_client.get(&url).send().await?;
self.token = AppResponse::from_response(resp).await?.into_data()?;
Ok(())
}
pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppError> { pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppError> {
let url = format!("{}/api/user/sign_up", self.base_url); let url = format!("{}/api/user/sign_up", self.base_url);
let payload = serde_json::json!({ let payload = serde_json::json!({
@ -239,15 +251,16 @@ impl Client {
} }
} }
fn http_client_with_auth(&self, method: Method, url: &str) -> Result<RequestBuilder, Error> { fn http_client_with_auth(&self, method: Method, url: &str) -> Result<RequestBuilder, AppError> {
match &self.token { match &self.token {
None => anyhow::bail!("no token found, are you logged in?"), None => Err(ErrorCode::NotLoggedIn.into()),
Some(t) => Ok( Some(t) => {
self let request_builder = self
.http_client .http_client
.request(method, url) .request(method, url)
.bearer_auth(t.access_token.to_string()), .bearer_auth(&t.access_token);
), Ok(request_builder)
},
} }
} }

View File

@ -1,6 +1,6 @@
pub enum Grant { pub enum Grant {
Password(PasswordGrant), Password(PasswordGrant),
RefreshToken, RefreshToken(RefreshTokenGrant),
IdToken, IdToken,
PKCE, PKCE,
} }
@ -10,11 +10,15 @@ pub struct PasswordGrant {
pub password: String, pub password: String,
} }
pub struct RefreshTokenGrant {
pub refresh_token: String,
}
impl Grant { impl Grant {
pub fn type_as_str(&self) -> &str { pub fn type_as_str(&self) -> &str {
match self { match self {
Grant::Password(_) => "password", Grant::Password(_) => "password",
Grant::RefreshToken => "refresh_token", Grant::RefreshToken(_) => "refresh_token",
Grant::IdToken => "id_token", Grant::IdToken => "id_token",
Grant::PKCE => "password", Grant::PKCE => "password",
} }
@ -28,7 +32,11 @@ impl Grant {
"password": p.password, "password": p.password,
}) })
}, },
Grant::RefreshToken => todo!(), Grant::RefreshToken(r) => {
serde_json::json!({
"refresh_token": r.refresh_token,
})
},
Grant::IdToken => todo!(), Grant::IdToken => todo!(),
Grant::PKCE => todo!(), Grant::PKCE => todo!(),
} }

View File

@ -48,6 +48,9 @@ pub enum ErrorCode {
#[error("Invalid OAuth Provider")] #[error("Invalid OAuth Provider")]
InvalidOAuthProvider = 1010, InvalidOAuthProvider = 1010,
#[error("Not Logged In")]
NotLoggedIn = 1011,
} }
/// Implements conversion from `anyhow::Error` to `ErrorCode`. /// Implements conversion from `anyhow::Error` to `ErrorCode`.

View File

@ -29,6 +29,7 @@ pub fn user_scope() -> Scope {
.service(web::resource("/update").route(web::post().to(update_handler))) .service(web::resource("/update").route(web::post().to(update_handler)))
.service(web::resource("/oauth/{provider}").route(web::get().to(oauth_handler))) .service(web::resource("/oauth/{provider}").route(web::get().to(oauth_handler)))
.service(web::resource("/info/{access_token}").route(web::get().to(info_handler))) .service(web::resource("/info/{access_token}").route(web::get().to(info_handler)))
.service(web::resource("/refresh/{refresh_token}").route(web::get().to(refresh_handler)))
.service(web::resource("/workspaces").route(web::get().to(workspaces_handler))) .service(web::resource("/workspaces").route(web::get().to(workspaces_handler)))
.service(web::resource("/profile").route(web::get().to(profile_handler))) .service(web::resource("/profile").route(web::get().to(profile_handler)))
@ -40,6 +41,15 @@ pub fn user_scope() -> Scope {
.service(web::resource("/password").route(web::post().to(change_password_handler))) .service(web::resource("/password").route(web::post().to(change_password_handler)))
} }
async fn refresh_handler(
path: web::Path<String>,
state: Data<State>,
) -> Result<JsonAppResponse<AccessTokenResponse>> {
let refresh_token = path.into_inner();
let oauth_url = biz::user::refresh(&state.gotrue_client, refresh_token).await?;
Ok(AppResponse::Ok().with_data(oauth_url).into())
}
async fn info_handler( async fn info_handler(
path: web::Path<String>, path: web::Path<String>,
state: Data<State>, state: Data<State>,

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use gotrue::{ use gotrue::{
api::Client, api::Client,
grant::{Grant, PasswordGrant}, grant::{Grant, PasswordGrant, RefreshTokenGrant},
}; };
use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User}; use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User};
use shared_entity::{ use shared_entity::{
@ -17,6 +17,15 @@ use crate::domain::validate_password;
use sqlx::{types::uuid, PgPool}; use sqlx::{types::uuid, PgPool};
use tracing::instrument; use tracing::instrument;
pub async fn refresh(
gotrue_client: &Client,
refresh_token: String,
) -> Result<AccessTokenResponse, AppError> {
let grant = Grant::RefreshToken(RefreshTokenGrant { refresh_token });
let token = gotrue_client.token(&grant).await??;
Ok(token)
}
#[instrument(level = "info", skip_all, err)] #[instrument(level = "info", skip_all, err)]
pub async fn sign_up( pub async fn sign_up(
gotrue_client: &Client, gotrue_client: &Client,

View File

@ -1,4 +1,5 @@
pub mod constants; pub mod constants;
mod refresh;
mod sign_in; mod sign_in;
mod sign_out; mod sign_out;
mod sign_up; mod sign_up;

15
tests/client/refresh.rs Normal file
View File

@ -0,0 +1,15 @@
use crate::{
client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX},
client_api_client,
};
#[tokio::test]
async fn refresh_success() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let email = &REGISTERED_EMAIL;
let password = &REGISTERED_PASSWORD;
let mut c = client_api_client();
c.sign_in_password(email, password).await.unwrap();
c.refresh().await.unwrap();
}

View File

@ -1,6 +1,8 @@
use shared_entity::error_code::ErrorCode; use shared_entity::error_code::ErrorCode;
use crate::client::utils::{generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{
generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX,
};
use crate::client_api_client; use crate::client_api_client;
#[tokio::test] #[tokio::test]
@ -47,6 +49,8 @@ async fn sign_in_unconfirmed_email() {
#[tokio::test] #[tokio::test]
async fn sign_in_success() { async fn sign_in_success() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await

View File

@ -1,4 +1,4 @@
use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX};
use crate::client_api_client; use crate::client_api_client;
#[tokio::test] #[tokio::test]
@ -10,8 +10,9 @@ async fn sign_out_but_not_sign_in() {
#[tokio::test] #[tokio::test]
async fn sign_out_after_sign_in() { async fn sign_out_after_sign_in() {
let mut c = client_api_client(); let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await
.unwrap(); .unwrap();

View File

@ -2,7 +2,9 @@ use gotrue_entity::OAuthProvider;
use shared_entity::error_code::ErrorCode; use shared_entity::error_code::ErrorCode;
use crate::{ use crate::{
client::utils::{generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD}, client::utils::{
generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX,
},
client_api_client, client_api_client,
}; };
@ -36,6 +38,8 @@ async fn sign_up_invalid_password() {
#[tokio::test] #[tokio::test]
async fn sign_up_but_existing_user() { async fn sign_up_but_existing_user() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let c = client_api_client(); let c = client_api_client();
c.sign_up(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_up(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await

View File

@ -1,4 +1,6 @@
use crate::client::utils::{generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{
generate_unique_email, REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX,
};
use crate::client_api_client; use crate::client_api_client;
#[tokio::test] #[tokio::test]
@ -12,6 +14,8 @@ async fn update_but_not_logged_in() {
#[tokio::test] #[tokio::test]
async fn update_password_same_password() { async fn update_password_same_password() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await
@ -23,6 +27,8 @@ async fn update_password_same_password() {
#[tokio::test] #[tokio::test]
async fn update_password_and_revert() { async fn update_password_and_revert() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let new_password = "Hello456!"; let new_password = "Hello456!";
{ {
// change password to new_password // change password to new_password

View File

@ -1,5 +1,6 @@
use dotenv::dotenv; use dotenv::dotenv;
use std::time::SystemTime; use std::time::SystemTime;
use tokio::sync::Mutex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -12,6 +13,7 @@ lazy_static! {
dotenv().ok(); dotenv().ok();
std::env::var("GOTRUE_REGISTERED_PASSWORD").unwrap() std::env::var("GOTRUE_REGISTERED_PASSWORD").unwrap()
}; };
pub static ref REGISTERED_USER_MUTEX: Mutex<()> = Mutex::new(());
} }
pub fn timestamp_nano() -> u128 { pub fn timestamp_nano() -> u128 {

View File

@ -1,4 +1,4 @@
use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX};
use crate::client_api_client; use crate::client_api_client;
use crate::collab::workspace_id_from_client; use crate::collab::workspace_id_from_client;
@ -9,6 +9,8 @@ use uuid::Uuid;
#[tokio::test] #[tokio::test]
async fn success_insert_collab_test() { async fn success_insert_collab_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await
@ -40,6 +42,8 @@ async fn success_insert_collab_test() {
#[tokio::test] #[tokio::test]
async fn success_delete_collab_test() { async fn success_delete_collab_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await
@ -77,6 +81,8 @@ async fn success_delete_collab_test() {
#[tokio::test] #[tokio::test]
async fn fail_insert_collab_with_empty_payload_test() { async fn fail_insert_collab_with_empty_payload_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await
@ -99,6 +105,8 @@ async fn fail_insert_collab_with_empty_payload_test() {
#[tokio::test] #[tokio::test]
async fn fail_insert_collab_with_invalid_workspace_id_test() { async fn fail_insert_collab_with_invalid_workspace_id_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await

View File

@ -1,10 +1,12 @@
use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX};
use crate::client_api_client; use crate::client_api_client;
use collab_ws::{ConnectState, WSClient, WSClientConfig}; use collab_ws::{ConnectState, WSClient, WSClientConfig};
#[tokio::test] #[tokio::test]
async fn realtime_connect_test() { async fn realtime_connect_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await .await

View File

@ -1,4 +1,4 @@
use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD}; use crate::client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX};
use client_api::Client; use client_api::Client;
use collab::core::collab::MutexCollab; use collab::core::collab::MutexCollab;
@ -26,6 +26,8 @@ pub(crate) struct TestClient {
impl TestClient { impl TestClient {
pub(crate) async fn new(client: &mut Client, object_id: &str, collab_type: CollabType) -> Self { pub(crate) async fn new(client: &mut Client, object_id: &str, collab_type: CollabType) -> Self {
let _guard = REGISTERED_USER_MUTEX.lock().await;
// Sign in // Sign in
client client
.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD) .sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)