From d638c0176399b078b7f8ccec9e456b1bc6ef0401 Mon Sep 17 00:00:00 2001 From: Zack <33050391+speed2exe@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:21:06 +0800 Subject: [PATCH] feat: added oauth login for admin (#119) * doc: added deployment guide for appflowy cloud * feat: added oauth login for admin * feat: specify redirect_url * feat: implemented google oauth * fix: default value for redirect_to * fix: add check for location hash --- README.md | 47 +---------- admin_frontend/dev/nginx.conf | 32 -------- admin_frontend/src/web_api.rs | 57 ++++++++++++- admin_frontend/templates/login.html | 30 +++++++ doc/README.md | 4 + doc/deployment.md | 120 ++++++++++++++++++++++++++++ doc/integration.md | 1 + docker-compose-dev.yml | 3 +- docker-compose.yml | 3 +- docker/gotrue.Dockerfile | 2 +- libs/client-api/src/http.rs | 36 +++------ libs/gotrue/src/params.rs | 19 ++++- nginx/nginx.conf | 3 + 13 files changed, 247 insertions(+), 110 deletions(-) delete mode 100644 admin_frontend/dev/nginx.conf create mode 100644 doc/README.md create mode 100644 doc/deployment.md create mode 100644 doc/integration.md diff --git a/README.md b/README.md index 60f4bbb5..8bbc3b42 100644 --- a/README.md +++ b/README.md @@ -2,52 +2,9 @@ - Cloud Server for AppFlowy ## Deployment +- See [deployment guide](./doc/deployment.md) -### Environmental Variables before starting -- you can set it explicitly(below) or in a `.env` file (use `dev.env`) as template -```bash -# authentication key, change this and keep the key safe and secret -GOTRUE_JWT_SECRET=secret_auth_pass - -# enabled by default, if you dont want need email confirmation, set to false -GOTRUE_MAILER_AUTOCONFIRM=true - -# if you enable mail confirmation, you need to set the SMTP configuration below -GOTRUE_SMTP_HOST=smtp.gmail.com -GOTRUE_SMTP_PORT=465 -GOTRUE_SMTP_USER=email_sender@some_company.com -GOTRUE_SMTP_PASS=email_sender_password -GOTRUE_SMTP_ADMIN_EMAIL=comp_admin@@some_company.com - -# Change 'localhost:9998' to the public host of machine that is running on. -# This is for email confirmation link -API_EXTERNAL_URL=http://localhost:9998 - -# Enable Google OAuth2, default: false, quick link for set up: -# https://console.cloud.google.com/apis/credentials -# https://console.cloud.google.com/apis/credentials/consent -GOTRUE_EXTERNAL_GOOGLE_ENABLED=false -GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=some_id -GOTRUE_EXTERNAL_GOOGLE_SECRET=some_secret -# Change 'localhost:9998' to the public host of machine that is running on. -GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=http://localhost:9998/callback -``` -- additional settings can be modified in `docker-compose.yml` -## SSL Certificate -- To use your own SSL, replace `certificate.crt` and `private_key.key` -with your own in `nginx/ssl/` directory - -### Start Cloud Server -```bash -docker-compose up -d -``` - -### Ports -Host Server is required to expose the following Ports: -- `443` (https) -- `80` (http) - -## Local Development +## Development ### Pre-requisites diff --git a/admin_frontend/dev/nginx.conf b/admin_frontend/dev/nginx.conf deleted file mode 100644 index c968826a..00000000 --- a/admin_frontend/dev/nginx.conf +++ /dev/null @@ -1,32 +0,0 @@ -# Minimal nginx configuration for AppFlowy-Cloud -# Self Hosted AppFlowy Cloud user should alter this file to suit their needs - -events { - worker_connections 1024; -} - -http { - server { - listen 80; - server_name gotrue; - - location / { - proxy_pass http://localhost:9998; - } - } - - server { - listen 80; - - # GoTrue - location ~ ^/(verify|authorize|callback|settings|user|token|admin) { - proxy_pass http://localhost:9998; - } - - # Admin Frontend - location / { - proxy_pass http://localhost:3000; - } - } - -} diff --git a/admin_frontend/src/web_api.rs b/admin_frontend/src/web_api.rs index c753a506..9161be36 100644 --- a/admin_frontend/src/web_api.rs +++ b/admin_frontend/src/web_api.rs @@ -4,7 +4,7 @@ use crate::response::WebApiResponse; use crate::session::{self, UserSession}; use crate::{models::LoginRequest, AppState}; use axum::extract::Path; -use axum::http::status; +use axum::http::{status, HeaderMap, HeaderValue}; use axum::response::Result; use axum::Json; use axum::{extract::State, routing::post, Router}; @@ -19,9 +19,38 @@ pub fn router() -> Router { Router::new() // TODO .route("/login", post(login_handler)) + .route("/login_refresh/:refresh_token", post(login_refresh_handler)) .route("/logout", post(logout_handler)) .route("/user/:param", post(post_user_handler).delete(delete_user_handler).put(put_user_handler)) .route("/user/:email/generate-link", post(post_user_generate_link_handler)) + .route("/oauth_login/:provider", post(post_oauth_login_handler)) +} + +static DEFAULT_HOST: HeaderValue = HeaderValue::from_static("localhost"); +static DEFAULT_SCHEME: HeaderValue = HeaderValue::from_static("http"); +pub async fn post_oauth_login_handler( + header_map: HeaderMap, + Path(provider): Path, +) -> Result, WebApiError<'static>> { + let host = header_map + .get("host") + .unwrap_or(&DEFAULT_HOST) + .to_str() + .unwrap(); + let scheme = header_map + .get("x-scheme") + .unwrap_or(&DEFAULT_SCHEME) + .to_str() + .unwrap(); + let base_url = format!("{}://{}", scheme, host); + + let oauth_url = format!( + "{}/authorize?provider={}&redirect_uri={}", + base_url, + &provider, + format!("{}/web/oauth_login_redirect", base_url) + ); + Ok(oauth_url.into()) } pub async fn put_user_handler( @@ -98,6 +127,32 @@ pub async fn post_user_handler( Ok(user.into()) } +pub async fn login_refresh_handler( + State(state): State, + jar: CookieJar, + Path(refresh_token): Path, +) -> Result> { + let token = state + .gotrue_client + .token(&gotrue::grant::Grant::RefreshToken( + gotrue::grant::RefreshTokenGrant { refresh_token }, + )) + .await?; + + let new_session_id = uuid::Uuid::new_v4(); + let new_session = session::UserSession::new( + new_session_id.to_string(), + token.access_token.to_string(), + token.refresh_token.to_owned(), + ); + state.session_store.put_user_session(&new_session).await?; + + let mut cookie = Cookie::new("session_id", new_session_id.to_string()); + cookie.set_path("/"); + + Ok(jar.add(cookie)) +} + // TODO: Support OAuth2 login // login and set the cookie pub async fn login_handler( diff --git a/admin_frontend/templates/login.html b/admin_frontend/templates/login.html index 6bcde6d3..e0fa2fd8 100644 --- a/admin_frontend/templates/login.html +++ b/admin_frontend/templates/login.html @@ -25,6 +25,11 @@
+
+

Or login with:

+ Google +
+ diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..c33da2ad --- /dev/null +++ b/doc/README.md @@ -0,0 +1,4 @@ +# Docs +- Directory to contain information about usage and development. +- [Appflowy Cloud Deployment](./deployment.md) +- [Appflowy with Cloud](./integration.md) diff --git a/doc/deployment.md b/doc/deployment.md new file mode 100644 index 00000000..a9cd989c --- /dev/null +++ b/doc/deployment.md @@ -0,0 +1,120 @@ +# Deployment +- AppFlowy-Cloud is designed to be easily self deployed for self managed cloud storage +- The following document will walk you through the steps to deploy your own AppFlowy-Cloud + +## Hardware Requirements +- Because AppFlowy-Cloud will have to be running persistently (or at least when one of the user is using), +we recommend using cloud compute services (as your host server) such as + - [Amazon EC2](https://aws.amazon.com/ec2/) or + - [Azure Virtual Machines](https://azure.microsoft.com/en-gb/products/virtual-machines/) +- Minimum 2GB Ram (4GB Recommended) +- Ports 80/443 available + +## Software Requirements +- [docker compose](https://docs.docker.com/compose) +This is needed be installed in your host server + +## Steps + +### 1. Getting source files +- Clone this repository into your host server and `cd` into it +```bash +git clone https://github.com/AppFlowy-IO/AppFlowy-Cloud` +cd AppFlowy-Cloud` +``` + +### 2. Preparing the configuration +- This is perhaps the most important part of the deployment process, please read carefully. +- It is required that that is a `.env` file in the root directory of the repository. +- To get started, copy the template `dev.env` as `.env` using the following shell commands: +```bash +cp dev.env .env +``` +- There will be values in the `.env` that needs to be change according to your needs +- Kindly read the following comments for each set of settings +```bash +# This is the secret key for authentication, please change this and keep the key safe +GOTRUE_JWT_SECRET=hello456 + +# This determine if the user will be user automatically be confirmed when they sign up +# If this is enabled, it requires a clicking a confirmation link in the email which user +# use for sign up. +# Pre-requisite if you enable: you need to have your SMTP Service set up, +# which you can then fill in the details below +GOTRUE_MAILER_AUTOCONFIRM=true + +# if you enable mail confirmation, you need to set the SMTP configuration below +GOTRUE_SMTP_HOST=smtp.gmail.com +GOTRUE_SMTP_PORT=465 +GOTRUE_SMTP_USER=user1@example.com +# this is typically an app password that you would need to generate: https://myaccount.google.com/apppasswords +GOTRUE_SMTP_PASS=somesecretkey +# You can leave this field same as GOTRUE_SMTP_USER +GOTRUE_SMTP_ADMIN_EMAIL=user1@example.com + +# This is the email account that is the admin account +# which has the highest privilege level, typically use to +# manage other users, such as user creation, deletion, password change, etc +GOTRUE_ADMIN_EMAIL=admin@example.com +GOTRUE_ADMIN_PASSWORD=password + +# This is the address of the authentication server +# which is the same as the public IP/hostname of your host server +# when an email confirmation link is click, this is the host that user's devices +# will try to connect to +API_EXTERNAL_URL=http://localhost:9998 + +# 2 fields below are only relevant for development, can ignore +DATABASE_URL=postgres://postgres:password@localhost:5433/postgres +SQLX_OFFLINE=false + +# Google OAuth2 +# This enables login using user's google account +# To set up, you need to go the following sites: +# https://console.cloud.google.com/apis/credentials/consent +# https://console.cloud.google.com/apis/credentials -> create credentials -> create oauth client ID +# in the field `Authorised redirect URIs`, you should put `/callback` +GOTRUE_EXTERNAL_GOOGLE_ENABLED=false +GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID= +GOTRUE_EXTERNAL_GOOGLE_SECRET= +GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=http://localhost:9998/callback + +# File Storage +# This affects where the files will be uploaded. +# By default, Minio will be deployed as file storage server # and it will use the host server's disk storage. +# You can also AWS S3 by setting USE_MINIO as false +USE_MINIO=true # determine if minio-server is used +# MINIO_URL=http://localhost:9000 # change this to use minio from a different host (e.g. maybe you self host Minio somewhere) +AWS_ACCESS_KEY_ID=minioadmin +AWS_SECRET_ACCESS_KEY=minioadmin +AWS_S3_BUCKET=appflowy +AWS_REGION=us-east-1 # This option only applicable for AWS S3 +``` + +### 3. Running the services + +### Start and run AppFlowy-Cloud +- The following command will build and start the AppFlowy-Cloud +```bash +docker compose up -d +``` +- Please check that all the services are running +```bash +docker ps -a +``` + +### 4. Reconfiguring and redeployment +- It is very common to reconfigure and restart. To do so, simply edit the `.env` and do `docker compose up -d` again + +## Ports +- After Deployment, you should see that AppFlowy-Cloud is serving 2 ports +- `443` (https) +- `80` (http) +- Your host server need to expose either of the port + +## SSL Certificate +- To use your own SSL certications for https, replace `certificate.crt` and `private_key.key` +with your own in `nginx/ssl/` directory + +## Usage of AppFlowy Application with AppFlowy Cloud +- [AppFlowy with AppFlowyCloud](./integration.md) diff --git a/doc/integration.md b/doc/integration.md new file mode 100644 index 00000000..c9173893 --- /dev/null +++ b/doc/integration.md @@ -0,0 +1 @@ +# Using AppFlowy with AppFlowy Cloud diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 1a9ca229..16e228dd 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -46,7 +46,8 @@ services: depends_on: - postgres environment: - - GOTRUE_SITE_URL=appflowy-flutter:// # redirected to AppFlowy application + - GOTRUE_SITE_URL= # redirected to AppFlowy application + - URI_ALLOW_LIST=* # adjust restrict if necessary - GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET} # authentication secret - GOTRUE_DB_DRIVER=postgres - API_EXTERNAL_URL=${API_EXTERNAL_URL} diff --git a/docker-compose.yml b/docker-compose.yml index b4f8f206..e9af9029 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,8 @@ services: depends_on: - postgres environment: - - GOTRUE_SITE_URL=appflowy-flutter:// # redirected to AppFlowy application + - GOTRUE_SITE_URL= # redirected to AppFlowy application + - URI_ALLOW_LIST=* # adjust restrict if necessary - GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET} # authentication secret - GOTRUE_DB_DRIVER=postgres - API_EXTERNAL_URL=${API_EXTERNAL_URL} diff --git a/docker/gotrue.Dockerfile b/docker/gotrue.Dockerfile index 56760b22..5e341c71 100644 --- a/docker/gotrue.Dockerfile +++ b/docker/gotrue.Dockerfile @@ -2,5 +2,5 @@ FROM golang WORKDIR /go/src/supabase RUN git clone https://github.com/supabase/gotrue.git WORKDIR /go/src/supabase/gotrue -RUN git checkout v2.95.2 && go install +RUN git checkout v2.99.0 && go install CMD ["gotrue"] diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 861db5b5..66269ce7 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -124,28 +124,14 @@ impl Client { .try_for_each(|f| -> Result<(), AppError> { let (k, v) = f.split_once('=').ok_or(url_missing_param("key=value"))?; match k { - "access_token" => { - access_token = Some(v.to_string()); - }, - "token_type" => { - token_type = Some(v.to_string()); - }, - "expires_in" => { - expires_in = Some(v.parse::().context("parser expires_in failed")?); - }, - "expires_at" => { - expires_at = Some(v.parse::().context("parser expires_at failed")?); - }, - "refresh_token" => { - refresh_token = Some(v.to_string()); - }, - "provider_access_token" => { - provider_access_token = Some(v.to_string()); - }, - "provider_refresh_token" => { - provider_refresh_token = Some(v.to_string()); - }, - _ => {}, + "access_token" => access_token = Some(v.to_string()), + "token_type" => token_type = Some(v.to_string()), + "expires_in" => expires_in = Some(v.parse::().context("parser expires_in failed")?), + "expires_at" => expires_at = Some(v.parse::().context("parser expires_at failed")?), + "refresh_token" => refresh_token = Some(v.to_string()), + "provider_access_token" => provider_access_token = Some(v.to_string()), + "provider_refresh_token" => provider_refresh_token = Some(v.to_string()), + x => tracing::warn!("unhandled param in url: {}", x), }; Ok(()) })?; @@ -196,7 +182,7 @@ impl Client { } Ok(format!( - "{}/authorize?provider={}", + "{}/authorize?provider={}&redirect_to=appflowy-flutter://", self.gotrue_client.base_url, provider.as_str(), )) @@ -760,10 +746,10 @@ pub fn extract_sign_in_url(html_str: &str) -> Result { let url = fragment .select(&selector) .next() - .ok_or(anyhow!("no a tag found"))? + .ok_or(anyhow!("no a tag found in html: {}", html_str))? .value() .attr("href") - .ok_or(anyhow!("no href found"))? + .ok_or(anyhow!("no href found in html: {}", html_str))? .to_string(); Ok(url) } diff --git a/libs/gotrue/src/params.rs b/libs/gotrue/src/params.rs index 329297bb..3afcc21c 100644 --- a/libs/gotrue/src/params.rs +++ b/libs/gotrue/src/params.rs @@ -22,7 +22,7 @@ pub struct AdminUserParams { pub ban_duration: String, } -#[derive(Default, Deserialize, Serialize)] +#[derive(Deserialize, Serialize)] pub struct GenerateLinkParams { #[serde(rename = "type")] pub type_: GenerateLinkType, @@ -34,12 +34,23 @@ pub struct GenerateLinkParams { pub redirect_to: String, } -#[derive(Default, Deserialize, Serialize)] +impl Default for GenerateLinkParams { + fn default() -> Self { + GenerateLinkParams { + type_: GenerateLinkType::MagicLink, + email: String::default(), + new_email: String::default(), + password: String::default(), + data: BTreeMap::new(), + redirect_to: "appflowy-flutter://".to_string(), + } + } +} + +#[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum GenerateLinkType { - #[default] MagicLink, - Recovery, Invite, Signup, diff --git a/nginx/nginx.conf b/nginx/nginx.conf index dcf0a209..2b65f9e4 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -49,6 +49,9 @@ http { # Admin Frontend location / { + proxy_set_header X-Scheme $scheme; + proxy_set_header Host $host; + proxy_pass http://admin_frontend:3000; } }