diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index f3a5ae37..6427a0f2 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -59,6 +59,9 @@ pub struct Client { token: Arc>, } +/// Hardcoded schema in the frontend application. Do not change this value. +const DESKTOP_CALLBACK_URL: &str = "appflowy-flutter://login-callback"; + impl Client { /// Constructs a new `Client` instance. /// @@ -165,7 +168,9 @@ impl Client { /// /// For example, the OAuth URL on Google looks like `https://appflowy.io/authorize?provider=google`. /// The deep link looks like `appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer`. - + /// + /// The appflowy-flutter:// is a hardcoded schema in the frontend application + /// /// # Parameters /// - `provider`: A reference to an `OAuthProvider` indicating which OAuth provider to use for login. /// @@ -183,11 +188,30 @@ impl Client { return Err(ErrorCode::InvalidOAuthProvider.into()); } - Ok(format!( - "{}/authorize?provider={}&redirect_to=appflowy-flutter://", - self.gotrue_client.base_url, - provider.as_str(), - )) + let url = format!("{}/authorize", self.gotrue_client.base_url,); + + let mut url = Url::parse(&url)?; + url + .query_pairs_mut() + .append_pair("provider", provider.as_str()) + .append_pair("redirect_to", DESKTOP_CALLBACK_URL); + + if let OAuthProvider::Google = provider { + url + .query_pairs_mut() + // In many cases, especially for server-side applications or mobile apps that might need to + // interact with Google services on behalf of the user without the user being actively + // engaged, access_type=offline is preferred to ensure long-term access. + .append_pair("access_type", "offline") + // In Google OAuth2.0, the prompt parameter is used to control the OAuth2.0 flow's behavior. + // It determines if the user is re-prompted for authentication and/or consent. + // 1. none: The authorization server does not display any authentication or consent user interface pages. + // 2. consent: The authorization server prompts the user for consent before returning information to the client + // 3. select_account: The authorization server prompts the user to select a user account. + .append_pair("prompt", "consent"); + } + + Ok(url.to_string()) } /// Returns an OAuth URL by constructing the authorization URL for the specified provider. @@ -500,7 +524,7 @@ impl Client { .token .read() .as_ref() - .ok_or::(ErrorCode::NotLoggedIn.into())? + .ok_or(AppError::new(ErrorCode::NotLoggedIn, "No access token"))? .refresh_token .as_str() .to_owned(); diff --git a/libs/client-api/src/ws/client.rs b/libs/client-api/src/ws/client.rs index 38aba734..288970db 100644 --- a/libs/client-api/src/ws/client.rs +++ b/libs/client-api/src/ws/client.rs @@ -244,7 +244,12 @@ struct RetryCondition { addr: Weak>>, } impl Condition for RetryCondition { - fn should_retry(&mut self, _error: &WSError) -> bool { + fn should_retry(&mut self, error: &WSError) -> bool { + if let WSError::AuthError(err) = error { + debug!("WSClient auth error: {}, stop retry connn", err); + return false; + } + let should_retry = self .addr .upgrade() diff --git a/libs/client-api/src/ws/error.rs b/libs/client-api/src/ws/error.rs index 2951b403..9fe5a772 100644 --- a/libs/client-api/src/ws/error.rs +++ b/libs/client-api/src/ws/error.rs @@ -1,13 +1,18 @@ use crate::ws::ClientRealtimeMessage; +use reqwest::StatusCode; +use tokio_tungstenite::tungstenite::Error; #[derive(Debug, thiserror::Error)] pub enum WSError { - #[error(transparent)] - Tungstenite(#[from] tokio_tungstenite::tungstenite::error::Error), - #[error("Unsupported ws message type")] UnsupportedMsgType, + #[error(transparent)] + TungsteniteError(Error), + + #[error("Auth error: {0}")] + AuthError(String), + #[error(transparent)] SerdeError(#[from] serde_json::Error), @@ -17,3 +22,14 @@ pub enum WSError { #[error("Internal failure: {0}")] Internal(#[from] Box), } + +impl From for WSError { + fn from(value: Error) -> Self { + if let Error::Http(resp) = &value { + if resp.status() == StatusCode::UNAUTHORIZED { + return WSError::AuthError("Unauthorized ws connection".to_string()); + } + } + WSError::TungsteniteError(value) + } +} diff --git a/libs/shared-entity/src/app_error.rs b/libs/shared-entity/src/app_error.rs index 5404162e..4d8f12e2 100644 --- a/libs/shared-entity/src/app_error.rs +++ b/libs/shared-entity/src/app_error.rs @@ -44,7 +44,6 @@ impl actix_web::error::ResponseError for AppError { actix_web::HttpResponse::Ok().json(self) } } -// impl From for AppError { fn from(err: anyhow::Error) -> Self { match err.downcast::() { diff --git a/src/api/ws.rs b/src/api/ws.rs index d3d15448..f7bb241d 100644 --- a/src/api/ws.rs +++ b/src/api/ws.rs @@ -14,6 +14,7 @@ use crate::component::auth::jwt::{authorization_from_token, UserUuid}; use database::user::select_uid_from_uuid; use shared_entity::app_error::AppError; use std::time::Duration; +use tracing::instrument; pub fn ws_scope() -> Scope { web::scope("/ws").service(establish_ws_connection) @@ -24,6 +25,8 @@ const MAX_FRAME_SIZE: usize = 65_536; // 64 KiB type CollabServerData = Data< Addr, Arc>>, >; + +#[instrument(skip_all, err)] #[get("/{token}/{device_id}")] pub async fn establish_ws_connection( request: HttpRequest, diff --git a/src/component/auth/jwt.rs b/src/component/auth/jwt.rs index 4da35c22..c6352487 100644 --- a/src/component/auth/jwt.rs +++ b/src/component/auth/jwt.rs @@ -8,6 +8,7 @@ use sqlx::types::{uuid, Uuid}; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::str::FromStr; +use tracing::instrument; use crate::state::AppState; @@ -126,6 +127,7 @@ fn get_auth_from_request(req: &HttpRequest) -> Result, @@ -137,6 +139,7 @@ pub fn authorization_from_token( }) } +#[instrument(skip_all, err)] fn gotrue_jwt_claims_from_token( token: &str, state: &Data,