diff --git a/.github/workflows/wasm_publish.yml b/.github/workflows/wasm_publish.yml new file mode 100644 index 00000000..2f27585f --- /dev/null +++ b/.github/workflows/wasm_publish.yml @@ -0,0 +1,69 @@ +name: Manual NPM Package Publish + +on: + workflow_dispatch: + inputs: + working_directory: + description: 'Working directory (e.g., libs/client-api-wasm)' + required: true + default: 'libs/client-api-wasm' + package_name: + description: 'Which package to publish' + required: true + default: '@appflowyinc/client-api-wasm' + type: choice + options: + - '@appflowyinc/client-api-wasm' + package_version: + description: 'Package version' + required: true + +env: + NODE_VERSION: '20.12.0' + RUST_TOOLCHAIN: "1.77.1" +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + AppFlowy-Cloud + + - name: Install wasm-pack + run: cargo install wasm-pack + + - name: Build with wasm-pack + run: wasm-pack build --release + working-directory: ${{ github.event.inputs.working_directory }} + + - name: Update name + working-directory: ${{ github.event.inputs.working_directory }}/pkg + run: | + jq '.name = "${{ github.event.inputs.package_name }}"' package.json > package.json.tmp + mv package.json.tmp package.json + - name: Update version + working-directory: ${{ github.event.inputs.working_directory }}/pkg + run: | + npm version ${{ github.event.inputs.package_version }} + + - name: Configure npm for wasm-pack + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ${{ github.event.inputs.working_directory }}/pkg/.npmrc + + - name: Publish package + run: | + npm config set access public + wasm-pack publish + working-directory: ${{ github.event.inputs.working_directory }}/pkg + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/libs/client-api-wasm/Cargo.toml b/libs/client-api-wasm/Cargo.toml index 6f82792b..0a7913bf 100644 --- a/libs/client-api-wasm/Cargo.toml +++ b/libs/client-api-wasm/Cargo.toml @@ -8,18 +8,18 @@ edition = "2018" crate-type = ["cdylib", "rlib"] [dependencies] -wasm-bindgen = "0.2.84" +wasm-bindgen = "0.2.90" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. -console_error_panic_hook = { version = "0.1.7", optional = true } -serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.64" +console_error_panic_hook = { version = "0.1.7" } +serde.workspace = true +serde_json.workspace = true client-api = { path = "../client-api" } lazy_static = "1.4.0" -wasm-bindgen-futures = "0.4.20" +wasm-bindgen-futures = "0.4.40" tsify = "0.4.5" tracing.workspace = true bytes.workspace = true @@ -29,12 +29,11 @@ uuid.workspace = true database-entity.workspace = true collab-rt-entity.workspace = true collab-entity.workspace = true -serde_repr = "0.1.18" -wee_alloc = { version = "0.4.5", optional = true } +serde_repr = "0.1.19" +wee_alloc = { version = "0.4.5" } serde-wasm-bindgen = "0.6.5" [dev-dependencies] wasm-bindgen-test = "0.3.34" [features] default = [] -enable_wee_alloc = ["wee_alloc"] \ No newline at end of file diff --git a/libs/client-api-wasm/src/entities.rs b/libs/client-api-wasm/src/entities.rs index 87cafce2..c5e09b88 100644 --- a/libs/client-api-wasm/src/entities.rs +++ b/libs/client-api-wasm/src/entities.rs @@ -1,4 +1,4 @@ -use client_api::entity::AFUserProfile; +use client_api::entity::{AFUserProfile, AuthProvider}; use client_api::error::{AppResponseError, ErrorCode}; use collab_entity::{CollabType, EncodedCollab}; use database_entity::dto::{ @@ -237,3 +237,43 @@ impl From for BatchClientEncodeCollab { BatchClientEncodeCollab(hash_map) } } + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct PublishViewMeta { + pub data: String, +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct PublishViewPayload { + pub meta: PublishViewMeta, + /// The doc_state of the encoded collab. + pub data: Vec, +} +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct PublishInfo { + pub namespace: Option, + pub publish_name: String, +} +from_struct_for_jsvalue!(PublishViewMeta); +from_struct_for_jsvalue!(PublishViewPayload); +from_struct_for_jsvalue!(PublishInfo); + +pub fn parse_provider(provider: &str) -> AuthProvider { + match provider { + "google" => AuthProvider::Google, + "github" => AuthProvider::Github, + "discord" => AuthProvider::Discord, + _ => AuthProvider::Google, + } +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct OAuthURLResponse { + pub url: String, +} + +from_struct_for_jsvalue!(OAuthURLResponse); diff --git a/libs/client-api-wasm/src/lib.rs b/libs/client-api-wasm/src/lib.rs index 09bc584f..be3d900f 100644 --- a/libs/client-api-wasm/src/lib.rs +++ b/libs/client-api-wasm/src/lib.rs @@ -5,11 +5,13 @@ use crate::entities::*; use client_api::notify::TokenState; use client_api::{Client, ClientConfiguration}; use std::sync::Arc; +use uuid::Uuid; +use client_api::error::ErrorCode; +use console_error_panic_hook; use database_entity::dto::QueryCollab; use wasm_bindgen::prelude::*; -#[cfg(feature = "enable_wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; @@ -49,6 +51,7 @@ pub struct ClientAPI { impl ClientAPI { pub fn new(config: ClientAPIConfig) -> ClientAPI { tracing_wasm::set_as_global_default(); + console_error_panic_hook::set_once(); let configuration = ClientConfiguration::default(); if let Some(compression) = &config.configuration { @@ -115,6 +118,52 @@ impl ClientAPI { } } + pub async fn sign_in_with_magic_link( + &self, + email: &str, + redirect_to: &str, + ) -> Result<(), ClientResponse> { + match self + .client + .sign_in_with_magic_link(email, Some(redirect_to.to_string())) + .await + { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub async fn generate_oauth_url_with_provider( + &self, + provider: &str, + redirect_to: &str, + ) -> Result { + if provider.is_empty() { + return Err(ClientResponse { + code: ErrorCode::OAuthError, + message: "Provider is empty".to_string(), + }); + } + + let provider = parse_provider(provider); + + match self + .client + .generate_url_with_provider_and_redirect_to(&provider, Some(redirect_to.to_string())) + .await + { + Ok(url) => Ok(OAuthURLResponse { url }), + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub async fn sign_in_with_url(&self, url: &str) -> Result<(), ClientResponse> { + match self.client.sign_in_with_url(url).await { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), + } + } + pub async fn get_user(&self) -> Result { match self.client.get_profile().await { Ok(profile) => Ok(User::from(profile)), @@ -160,4 +209,57 @@ impl ClientAPI { Err(err) => Err(ClientResponse::from(err)), } } + + pub async fn get_publish_view_meta( + &self, + publish_namespace: String, + publish_name: String, + ) -> Result { + match self + .client + .get_published_collab::(publish_namespace.as_str(), publish_name.as_str()) + .await + { + Ok(data) => Ok(PublishViewMeta { + data: data.to_string(), + }), + + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub async fn get_publish_view( + &self, + publish_namespace: String, + publish_name: String, + ) -> Result { + let meta = self + .get_publish_view_meta(publish_namespace.clone(), publish_name.clone()) + .await?; + + let data = self + .client + .get_published_collab_blob(publish_namespace.as_str(), publish_name.as_str()) + .await + .map_err(ClientResponse::from)?; + + Ok(PublishViewPayload { + meta, + data: data.to_vec(), + }) + } + + pub async fn get_publish_info(&self, view_id: String) -> Result { + let view_id = Uuid::parse_str(view_id.as_str()).map_err(|err| ClientResponse { + code: ErrorCode::UuidError, + message: format!("Failed to parse view_id: {}", err), + })?; + match self.client.get_published_collab_info(&view_id).await { + Ok(info) => Ok(PublishInfo { + namespace: info.namespace, + publish_name: info.publish_name, + }), + Err(err) => Err(ClientResponse::from(err)), + } + } } diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 61b74d21..9db30dfc 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -293,6 +293,17 @@ impl Client { } /// Returns an OAuth URL by constructing the authorization URL for the specified provider. + #[instrument(level = "debug", skip_all, err)] + pub async fn generate_oauth_url_with_provider( + &self, + provider: &AuthProvider, + ) -> Result { + self + .generate_url_with_provider_and_redirect_to(provider, None) + .await + } + + /// Returns an OAuth URL by constructing the authorization URL for the specified provider and redirecting to the specified URL. /// /// 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, @@ -303,19 +314,19 @@ 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. + /// - `redirect_to`: An optional `String` containing the URL to redirect to after the user is authenticated. /// /// # Returns /// - `Ok(String)`: A `String` containing the constructed authorization URL if the specified provider is available. /// - `Err(AppResponseError)`: An `AppResponseError` indicating either the OAuth provider is invalid or other issues occurred while fetching settings. /// - #[instrument(level = "debug", skip_all, err)] - pub async fn generate_oauth_url_with_provider( + pub async fn generate_url_with_provider_and_redirect_to( &self, provider: &AuthProvider, + redirect_to: Option, ) -> Result { let settings = self.gotrue_client.settings().await?; if !settings.external.has_provider(provider) { @@ -328,7 +339,12 @@ impl Client { url .query_pairs_mut() .append_pair("provider", provider.as_str()) - .append_pair("redirect_to", DESKTOP_CALLBACK_URL); + .append_pair( + "redirect_to", + redirect_to + .unwrap_or_else(|| DESKTOP_CALLBACK_URL.to_string()) + .as_str(), + ); if let AuthProvider::Google = provider { url diff --git a/libs/client-api/src/http_publish.rs b/libs/client-api/src/http_publish.rs index 18165aa7..f2402a36 100644 --- a/libs/client-api/src/http_publish.rs +++ b/libs/client-api/src/http_publish.rs @@ -2,6 +2,7 @@ use bytes::Bytes; use client_api_entity::{PublishInfo, UpdatePublishNamespace}; use reqwest::Method; use shared_entity::response::{AppResponse, AppResponseError}; +use tracing::instrument; use crate::Client; @@ -65,6 +66,7 @@ impl Client { // Guest API (no login required) impl Client { + #[instrument(level = "debug", skip_all)] pub async fn get_published_collab_info( &self, view_id: &uuid::Uuid, @@ -77,6 +79,7 @@ impl Client { .into_data() } + #[instrument(level = "debug", skip_all)] pub async fn get_published_collab( &self, publish_namespace: &str, @@ -85,6 +88,11 @@ impl Client { where T: serde::de::DeserializeOwned, { + tracing::debug!( + "get_published_collab: {} {}", + publish_namespace, + publish_name + ); let url = format!( "{}/api/workspace/published/{}/{}", self.base_url, publish_namespace, publish_name @@ -104,14 +112,21 @@ impl Client { } let meta = serde_json::from_str::(&txt)?; + Ok(meta) } + #[instrument(level = "debug", skip_all)] pub async fn get_published_collab_blob( &self, publish_namespace: &str, publish_name: &str, ) -> Result { + tracing::debug!( + "get_published_collab_blob: {} {}", + publish_namespace, + publish_name + ); let url = format!( "{}/api/workspace/published/{}/{}/blob", self.base_url, publish_namespace, publish_name