feat: oauth provider (#86)
* chore: remove opener * chore: rename method * chore: add docs * chore: modify env
This commit is contained in:
parent
b0c213b5c0
commit
cf84557ebe
|
|
@ -32,6 +32,7 @@ jobs:
|
||||||
sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.GOTRUE_SMTP_USER }}/' .env
|
sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.GOTRUE_SMTP_USER }}/' .env
|
||||||
sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.GOTRUE_SMTP_PASS }}/' .env
|
sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.GOTRUE_SMTP_PASS }}/' .env
|
||||||
sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.GOTRUE_SMTP_ADMIN_EMAIL }}/' .env
|
sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.GOTRUE_SMTP_ADMIN_EMAIL }}/' .env
|
||||||
|
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
|
||||||
|
|
||||||
- name: Run Docker-Compose
|
- name: Run Docker-Compose
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"mime",
|
"mime",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"opener",
|
||||||
"openssl",
|
"openssl",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
|
|
@ -843,7 +844,6 @@ dependencies = [
|
||||||
"gotrue-entity",
|
"gotrue-entity",
|
||||||
"lib0",
|
"lib0",
|
||||||
"mime",
|
"mime",
|
||||||
"opener",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper",
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ assert-json-diff = "2.0.2"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
scraper = "0.17.1"
|
scraper = "0.17.1"
|
||||||
client-api = { path = "libs/client-api", features = ["collab-sync"] }
|
client-api = { path = "libs/client-api", features = ["collab-sync"] }
|
||||||
|
opener = "0.6.1"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "appflowy_cloud"
|
name = "appflowy_cloud"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ docker-compose --file ./docker-compose-dev.yml down
|
||||||
|
|
||||||
# Start the Docker Compose setup
|
# Start the Docker Compose setup
|
||||||
export GOTRUE_MAILER_AUTOCONFIRM=true
|
export GOTRUE_MAILER_AUTOCONFIRM=true
|
||||||
|
|
||||||
|
# Enable Google OAuth when running locally
|
||||||
|
export GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
|
||||||
|
|
||||||
docker-compose --file ./docker-compose-dev.yml up -d --build
|
docker-compose --file ./docker-compose-dev.yml up -d --build
|
||||||
|
|
||||||
# Keep pinging Postgres until it's ready to accept commands
|
# Keep pinging Postgres until it's ready to accept commands
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ gotrue = { path = "../gotrue" }
|
||||||
gotrue-entity = { path = "../gotrue-entity" }
|
gotrue-entity = { path = "../gotrue-entity" }
|
||||||
shared_entity = { path = "../shared-entity" }
|
shared_entity = { path = "../shared-entity" }
|
||||||
database-entity = { path = "../database-entity" }
|
database-entity = { path = "../database-entity" }
|
||||||
opener = "0.6.1"
|
|
||||||
url = "2.4.1"
|
url = "2.4.1"
|
||||||
tokio-stream = { version = "0.1.14" }
|
tokio-stream = { version = "0.1.14" }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
|
|
@ -39,5 +38,8 @@ collab-define = { version = "0.1.0" }
|
||||||
yrs = { version = "0.16.5", optional = true }
|
yrs = { version = "0.16.5", optional = true }
|
||||||
lib0 = { version = "0.16.3", features = ["lib0-serde"], optional = true }
|
lib0 = { version = "0.16.3", features = ["lib0-serde"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
collab-sync = ["collab", "yrs", "lib0"]
|
collab-sync = ["collab", "yrs", "lib0"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ where
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Err(err) => warn!("Send message failed error: {:?}", err),
|
Err(err) => trace!("Send message failed error: {:?}", err),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.notify()
|
self.notify()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::{anyhow, Context};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use gotrue::grant::Grant;
|
use gotrue::grant::Grant;
|
||||||
|
|
@ -31,6 +31,18 @@ use shared_entity::error::AppError;
|
||||||
use shared_entity::error_code::url_missing_param;
|
use shared_entity::error_code::url_missing_param;
|
||||||
use shared_entity::error_code::ErrorCode;
|
use shared_entity::error_code::ErrorCode;
|
||||||
|
|
||||||
|
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
|
||||||
|
///
|
||||||
|
/// It provides methods to perform actions like signing in, signing out, refreshing tokens,
|
||||||
|
/// and interacting with file storage and collaboration objects.
|
||||||
|
///
|
||||||
|
/// # Fields
|
||||||
|
/// - `cloud_client`: A `reqwest::Client` used for HTTP requests to the cloud.
|
||||||
|
/// - `gotrue_client`: A `gotrue::api::Client` used for interacting with the GoTrue API.
|
||||||
|
/// - `base_url`: The base URL for API requests.
|
||||||
|
/// - `ws_addr`: The WebSocket address for real-time communication.
|
||||||
|
/// - `token`: An `Arc<RwLock<ClientToken>>` managing the client's authentication token.
|
||||||
|
///
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub(crate) cloud_client: reqwest::Client,
|
pub(crate) cloud_client: reqwest::Client,
|
||||||
pub(crate) gotrue_client: gotrue::api::Client,
|
pub(crate) gotrue_client: gotrue::api::Client,
|
||||||
|
|
@ -40,12 +52,19 @@ pub struct Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn from(c: reqwest::Client, base_url: &str, ws_addr: &str, gotrue_url: &str) -> Self {
|
/// Constructs a new `Client` instance.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `base_url`: The base URL for API requests.
|
||||||
|
/// - `ws_addr`: The WebSocket address for real-time communication.
|
||||||
|
/// - `gotrue_url`: The URL for the GoTrue API.
|
||||||
|
pub fn new(base_url: &str, ws_addr: &str, gotrue_url: &str) -> Self {
|
||||||
|
let reqwest_client = reqwest::Client::new();
|
||||||
Self {
|
Self {
|
||||||
base_url: base_url.to_string(),
|
base_url: base_url.to_string(),
|
||||||
ws_addr: ws_addr.to_string(),
|
ws_addr: ws_addr.to_string(),
|
||||||
cloud_client: c.clone(),
|
cloud_client: reqwest_client.clone(),
|
||||||
gotrue_client: gotrue::api::Client::new(c, gotrue_url),
|
gotrue_client: gotrue::api::Client::new(reqwest_client, gotrue_url),
|
||||||
token: Arc::new(RwLock::new(ClientToken::new())),
|
token: Arc::new(RwLock::new(ClientToken::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,8 +73,10 @@ impl Client {
|
||||||
self.token.read().subscribe()
|
self.token.read().subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
// e.g. appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer
|
/// Attempts to sign in using a URL, extracting and validating the token parameters from the URL fragment.
|
||||||
pub async fn sign_in_url(&self, url: &str) -> Result<bool, AppError> {
|
/// It looks like, e.g., `appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer`.
|
||||||
|
///
|
||||||
|
pub async fn sign_in_with_url(&self, url: &str) -> Result<bool, AppError> {
|
||||||
let mut access_token: Option<String> = None;
|
let mut access_token: Option<String> = None;
|
||||||
let mut token_type: Option<String> = None;
|
let mut token_type: Option<String> = None;
|
||||||
let mut expires_in: Option<i64> = None;
|
let mut expires_in: Option<i64> = None;
|
||||||
|
|
@ -78,10 +99,10 @@ impl Client {
|
||||||
token_type = Some(v.to_string());
|
token_type = Some(v.to_string());
|
||||||
},
|
},
|
||||||
"expires_in" => {
|
"expires_in" => {
|
||||||
expires_in = Some(v.parse::<i64>()?);
|
expires_in = Some(v.parse::<i64>().context("parser expires_in failed")?);
|
||||||
},
|
},
|
||||||
"expires_at" => {
|
"expires_at" => {
|
||||||
expires_at = Some(v.parse::<i64>()?);
|
expires_at = Some(v.parse::<i64>().context("parser expires_at failed")?);
|
||||||
},
|
},
|
||||||
"refresh_token" => {
|
"refresh_token" => {
|
||||||
refresh_token = Some(v.to_string());
|
refresh_token = Some(v.to_string());
|
||||||
|
|
@ -114,12 +135,82 @@ impl Client {
|
||||||
Ok(new)
|
Ok(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an OAuth URL by constructing the authorization URL for the specified provider.
|
||||||
|
///
|
||||||
|
/// This asynchronous function communicates with the GoTrue client to retrieve settings and
|
||||||
|
/// validate the availability of the specified OAuth provider. If the provider is available,
|
||||||
|
/// it constructs and returns the OAuth URL. When the user opens the OAuth URL, it redirects to
|
||||||
|
/// the corresponding provider's OAuth web page. After the user is authenticated, the browser will open
|
||||||
|
/// a deep link to the AppFlowy app (iOS, macOS, etc.), which will call [Client::sign_in_with_url] to sign in.
|
||||||
|
///
|
||||||
|
/// 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`.
|
||||||
|
|
||||||
|
/// # Parameters
|
||||||
|
/// - `provider`: A reference to an `OAuthProvider` indicating which OAuth provider to use for login.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(String)`: A `String` containing the constructed authorization URL if the specified provider is available.
|
||||||
|
/// - `Err(AppError)`: An `AppError` indicating either the OAuth provider is invalid or other issues occurred while fetching settings.
|
||||||
|
///
|
||||||
|
pub async fn generate_oauth_url_with_provider(
|
||||||
|
&self,
|
||||||
|
provider: &OAuthProvider,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let settings = self.gotrue_client.settings().await?;
|
||||||
|
if !settings.external.has_provider(provider) {
|
||||||
|
return Err(ErrorCode::InvalidOAuthProvider.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{}/authorize?provider={}",
|
||||||
|
self.gotrue_client.base_url,
|
||||||
|
provider.as_str(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an OAuth URL by constructing the authorization URL for the specified provider.
|
||||||
|
/// The URL looks like, e.g., `appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer`.
|
||||||
|
///
|
||||||
|
pub async fn generate_sign_in_url_with_email(
|
||||||
|
&self,
|
||||||
|
admin_user_email: &str,
|
||||||
|
admin_user_password: &str,
|
||||||
|
user_email: &str,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let admin_token = self
|
||||||
|
.gotrue_client
|
||||||
|
.token(&Grant::Password(PasswordGrant {
|
||||||
|
email: admin_user_email.to_string(),
|
||||||
|
password: admin_user_password.to_string(),
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let admin_user_params: GenerateLinkParams = GenerateLinkParams {
|
||||||
|
email: user_email.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let link_resp = self
|
||||||
|
.gotrue_client
|
||||||
|
.generate_link(&admin_token.access_token, &admin_user_params)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(link_resp.email, user_email);
|
||||||
|
|
||||||
|
let action_link = link_resp.action_link;
|
||||||
|
let resp = reqwest::Client::new().get(action_link).send().await?;
|
||||||
|
let resp_text = resp.text().await?;
|
||||||
|
Ok(extract_sign_in_url(&resp_text)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
async fn verify_token(&self, access_token: &str) -> Result<(User, bool), AppError> {
|
async fn verify_token(&self, access_token: &str) -> Result<(User, bool), AppError> {
|
||||||
let user = self.gotrue_client.user_info(access_token).await?;
|
let user = self.gotrue_client.user_info(access_token).await?;
|
||||||
let is_new = self.verify_token_cloud(access_token).await?;
|
let is_new = self.verify_token_cloud(access_token).await?;
|
||||||
Ok((user, is_new))
|
Ok((user, is_new))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
async fn verify_token_cloud(&self, access_token: &str) -> Result<bool, AppError> {
|
async fn verify_token_cloud(&self, access_token: &str) -> Result<bool, AppError> {
|
||||||
let url = format!("{}/api/user/verify/{}", self.base_url, access_token);
|
let url = format!("{}/api/user/verify/{}", self.base_url, access_token);
|
||||||
let resp = self.cloud_client.get(&url).send().await?;
|
let resp = self.cloud_client.get(&url).send().await?;
|
||||||
|
|
@ -169,6 +260,15 @@ impl Client {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the expiration timestamp of the current token.
|
||||||
|
///
|
||||||
|
/// This function attempts to read the current token and, if successful, returns the expiration timestamp.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(i64)`: An `i64` representing the expiration timestamp of the token.
|
||||||
|
/// - `Err(AppError)`: An `AppError` indicating either an inability to read the token or that the user is not logged in.
|
||||||
|
///
|
||||||
|
#[inline]
|
||||||
pub fn token_expires_at(&self) -> Result<i64, AppError> {
|
pub fn token_expires_at(&self) -> Result<i64, AppError> {
|
||||||
match &self.token.try_read() {
|
match &self.token.try_read() {
|
||||||
None => Err(AppError::new(ErrorCode::Unhandled, "Failed to read token")),
|
None => Err(AppError::new(ErrorCode::Unhandled, "Failed to read token")),
|
||||||
|
|
@ -176,6 +276,14 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the access token string.
|
||||||
|
///
|
||||||
|
/// This function attempts to read the current token and, if successful, returns the access token string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(String)`: A `String` containing the access token.
|
||||||
|
/// - `Err(AppError)`: An `AppError` indicating either an inability to read the token or that the user is not logged in.
|
||||||
|
///
|
||||||
pub fn access_token(&self) -> Result<String, AppError> {
|
pub fn access_token(&self) -> Result<String, AppError> {
|
||||||
match &self.token.try_read() {
|
match &self.token.try_read() {
|
||||||
None => Err(AppError::new(ErrorCode::Unhandled, "Failed to read token")),
|
None => Err(AppError::new(ErrorCode::Unhandled, "Failed to read token")),
|
||||||
|
|
@ -189,21 +297,6 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn oauth_login(&self, provider: &OAuthProvider) -> Result<(), AppError> {
|
|
||||||
let settings = self.gotrue_client.settings().await?;
|
|
||||||
if !settings.external.has_provider(provider) {
|
|
||||||
return Err(ErrorCode::InvalidOAuthProvider.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let oauth_url = format!(
|
|
||||||
"{}/authorize?provider={}",
|
|
||||||
self.gotrue_client.base_url,
|
|
||||||
provider.as_str(),
|
|
||||||
);
|
|
||||||
opener::open(oauth_url)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn profile(&self) -> Result<AFUserProfileView, AppError> {
|
pub async fn profile(&self) -> Result<AFUserProfileView, AppError> {
|
||||||
let url = format!("{}/api/user/profile", self.base_url);
|
let url = format!("{}/api/user/profile", self.base_url);
|
||||||
let resp = self
|
let resp = self
|
||||||
|
|
@ -301,6 +394,11 @@ impl Client {
|
||||||
Ok(is_new)
|
Ok(is_new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refreshes the access token using the stored refresh token.
|
||||||
|
///
|
||||||
|
/// This function attempts to refresh the access token by sending a request to the authentication server
|
||||||
|
/// using the stored refresh token. If successful, it updates the stored access token with the new one
|
||||||
|
/// received from the server.
|
||||||
pub async fn refresh(&self) -> Result<(), AppError> {
|
pub async fn refresh(&self) -> Result<(), AppError> {
|
||||||
let refresh_token = self
|
let refresh_token = self
|
||||||
.token
|
.token
|
||||||
|
|
@ -415,37 +513,6 @@ impl Client {
|
||||||
Ok(format!("{}/{}/{}", self.ws_addr, access_token, device_id))
|
Ok(format!("{}/{}/{}", self.ws_addr, access_token, device_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_sign_in_callback_url(
|
|
||||||
&self,
|
|
||||||
admin_user_email: &str,
|
|
||||||
admin_user_password: &str,
|
|
||||||
user_email: &str,
|
|
||||||
) -> Result<String, AppError> {
|
|
||||||
let admin_token = self
|
|
||||||
.gotrue_client
|
|
||||||
.token(&Grant::Password(PasswordGrant {
|
|
||||||
email: admin_user_email.to_string(),
|
|
||||||
password: admin_user_password.to_string(),
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let admin_user_params: GenerateLinkParams = GenerateLinkParams {
|
|
||||||
email: user_email.to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let link_resp = self
|
|
||||||
.gotrue_client
|
|
||||||
.generate_link(&admin_token.access_token, &admin_user_params)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(link_resp.email, user_email);
|
|
||||||
|
|
||||||
let action_link = link_resp.action_link;
|
|
||||||
let resp = reqwest::Client::new().get(action_link).send().await?;
|
|
||||||
let resp_text = resp.text().await?;
|
|
||||||
Ok(extract_sign_in_url(&resp_text)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn put_file_storage_object(
|
pub async fn put_file_storage_object(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async fn sign_in_success() {
|
||||||
async fn sign_in_with_invalid_url() {
|
async fn sign_in_with_invalid_url() {
|
||||||
let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer";
|
let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer";
|
||||||
let c = client_api_client();
|
let c = client_api_client();
|
||||||
match c.sign_in_url(url_str).await {
|
match c.sign_in_with_url(url_str).await {
|
||||||
Ok(_) => panic!("should not be ok"),
|
Ok(_) => panic!("should not be ok"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
assert_eq!(e.code, ErrorCode::OAuthError);
|
assert_eq!(e.code, ErrorCode::OAuthError);
|
||||||
|
|
@ -106,8 +106,8 @@ async fn sign_in_with_url() {
|
||||||
let c = client_api_client();
|
let c = client_api_client();
|
||||||
let user_email = generate_unique_email();
|
let user_email = generate_unique_email();
|
||||||
let url_str = c
|
let url_str = c
|
||||||
.generate_sign_in_callback_url(&ADMIN_USER.email, &ADMIN_USER.password, &user_email)
|
.generate_sign_in_url_with_email(&ADMIN_USER.email, &ADMIN_USER.password, &user_email)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = c.sign_in_url(&url_str).await.unwrap();
|
let _ = c.sign_in_with_url(&url_str).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,24 @@ async fn sign_up_but_existing_user() {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sign_up_oauth_not_available() {
|
async fn sign_up_oauth_not_available() {
|
||||||
let c = client_api_client();
|
let c = client_api_client();
|
||||||
|
let err = c
|
||||||
|
.generate_oauth_url_with_provider(&OAuthProvider::Zoom)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
// Change Zoom to any other valid OAuth provider
|
// Change Zoom to any other valid OAuth provider
|
||||||
// to manually open the browser and login
|
// to manually open the browser and login
|
||||||
c.oauth_login(&OAuthProvider::Zoom)
|
err.code,
|
||||||
.await
|
|
||||||
.err()
|
|
||||||
.unwrap()
|
|
||||||
.code,
|
|
||||||
ErrorCode::InvalidOAuthProvider
|
ErrorCode::InvalidOAuthProvider
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sign_up_with_google_oauth() {
|
||||||
|
let c = client_api_client();
|
||||||
|
let _ = c
|
||||||
|
.generate_oauth_url_with_provider(&OAuthProvider::Google)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ async fn admin_generate_link_and_user_sign_in() {
|
||||||
let appflowy_sign_in_url = extract_sign_in_url(&resp_text).unwrap();
|
let appflowy_sign_in_url = extract_sign_in_url(&resp_text).unwrap();
|
||||||
|
|
||||||
let client = client_api_client();
|
let client = client_api_client();
|
||||||
let is_new = client.sign_in_url(&appflowy_sign_in_url).await.unwrap();
|
let is_new = client
|
||||||
|
.sign_in_with_url(&appflowy_sign_in_url)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(is_new);
|
assert!(is_new);
|
||||||
|
|
||||||
let workspaces = client.workspaces().await.unwrap();
|
let workspaces = client.workspaces().await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,5 @@ mod gotrue;
|
||||||
mod realtime;
|
mod realtime;
|
||||||
|
|
||||||
pub fn client_api_client() -> Client {
|
pub fn client_api_client() -> Client {
|
||||||
Client::from(
|
Client::new(LOCALHOST_URL, LOCALHOST_WS, LOCALHOST_GOTRUE)
|
||||||
reqwest::Client::new(),
|
|
||||||
LOCALHOST_URL,
|
|
||||||
LOCALHOST_WS,
|
|
||||||
LOCALHOST_GOTRUE,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue