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::OAuthURL;
use reqwest::Method;
@ -146,6 +145,19 @@ impl Client {
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> {
let url = format!("{}/api/user/sign_up", self.base_url);
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 {
None => anyhow::bail!("no token found, are you logged in?"),
Some(t) => Ok(
self
None => Err(ErrorCode::NotLoggedIn.into()),
Some(t) => {
let request_builder = self
.http_client
.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 {
Password(PasswordGrant),
RefreshToken,
RefreshToken(RefreshTokenGrant),
IdToken,
PKCE,
}
@ -10,11 +10,15 @@ pub struct PasswordGrant {
pub password: String,
}
pub struct RefreshTokenGrant {
pub refresh_token: String,
}
impl Grant {
pub fn type_as_str(&self) -> &str {
match self {
Grant::Password(_) => "password",
Grant::RefreshToken => "refresh_token",
Grant::RefreshToken(_) => "refresh_token",
Grant::IdToken => "id_token",
Grant::PKCE => "password",
}
@ -28,7 +32,11 @@ impl Grant {
"password": p.password,
})
},
Grant::RefreshToken => todo!(),
Grant::RefreshToken(r) => {
serde_json::json!({
"refresh_token": r.refresh_token,
})
},
Grant::IdToken => todo!(),
Grant::PKCE => todo!(),
}

View File

@ -48,6 +48,9 @@ pub enum ErrorCode {
#[error("Invalid OAuth Provider")]
InvalidOAuthProvider = 1010,
#[error("Not Logged In")]
NotLoggedIn = 1011,
}
/// 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("/oauth/{provider}").route(web::get().to(oauth_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("/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)))
}
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(
path: web::Path<String>,
state: Data<State>,

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use anyhow::Result;
use gotrue::{
api::Client,
grant::{Grant, PasswordGrant},
grant::{Grant, PasswordGrant, RefreshTokenGrant},
};
use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User};
use shared_entity::{
@ -17,6 +17,15 @@ use crate::domain::validate_password;
use sqlx::{types::uuid, PgPool};
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)]
pub async fn sign_up(
gotrue_client: &Client,

View File

@ -1,4 +1,5 @@
pub mod constants;
mod refresh;
mod sign_in;
mod sign_out;
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 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;
#[tokio::test]
@ -47,6 +49,8 @@ async fn sign_in_unconfirmed_email() {
#[tokio::test]
async fn sign_in_success() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.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;
#[tokio::test]
@ -10,8 +10,9 @@ async fn sign_out_but_not_sign_in() {
#[tokio::test]
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)
.await
.unwrap();

View File

@ -2,7 +2,9 @@ use gotrue_entity::OAuthProvider;
use shared_entity::error_code::ErrorCode;
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,
};
@ -36,6 +38,8 @@ async fn sign_up_invalid_password() {
#[tokio::test]
async fn sign_up_but_existing_user() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let c = client_api_client();
c.sign_up(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.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;
#[tokio::test]
@ -12,6 +14,8 @@ async fn update_but_not_logged_in() {
#[tokio::test]
async fn update_password_same_password() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await
@ -23,6 +27,8 @@ async fn update_password_same_password() {
#[tokio::test]
async fn update_password_and_revert() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let new_password = "Hello456!";
{
// change password to new_password

View File

@ -1,5 +1,6 @@
use dotenv::dotenv;
use std::time::SystemTime;
use tokio::sync::Mutex;
use lazy_static::lazy_static;
@ -12,6 +13,7 @@ lazy_static! {
dotenv().ok();
std::env::var("GOTRUE_REGISTERED_PASSWORD").unwrap()
};
pub static ref REGISTERED_USER_MUTEX: Mutex<()> = Mutex::new(());
}
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::collab::workspace_id_from_client;
@ -9,6 +9,8 @@ use uuid::Uuid;
#[tokio::test]
async fn success_insert_collab_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await
@ -40,6 +42,8 @@ async fn success_insert_collab_test() {
#[tokio::test]
async fn success_delete_collab_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await
@ -77,6 +81,8 @@ async fn success_delete_collab_test() {
#[tokio::test]
async fn fail_insert_collab_with_empty_payload_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.await
@ -99,6 +105,8 @@ async fn fail_insert_collab_with_empty_payload_test() {
#[tokio::test]
async fn fail_insert_collab_with_invalid_workspace_id_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.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 collab_ws::{ConnectState, WSClient, WSClientConfig};
#[tokio::test]
async fn realtime_connect_test() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let mut c = client_api_client();
c.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)
.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 collab::core::collab::MutexCollab;
@ -26,6 +26,8 @@ pub(crate) struct TestClient {
impl TestClient {
pub(crate) async fn new(client: &mut Client, object_id: &str, collab_type: CollabType) -> Self {
let _guard = REGISTERED_USER_MUTEX.lock().await;
// Sign in
client
.sign_in_password(&REGISTERED_EMAIL, &REGISTERED_PASSWORD)