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
This commit is contained in:
Zack 2023-10-17 10:21:06 +08:00 committed by GitHub
parent 88be0c2433
commit d638c01763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 247 additions and 110 deletions

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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<AppState> {
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<String>,
) -> Result<WebApiResponse<String>, 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<AppState>,
jar: CookieJar,
Path(refresh_token): Path<String>,
) -> Result<CookieJar, WebApiError<'static>> {
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(

View File

@ -25,6 +25,11 @@
</form>
<div id="response"></div>
<div id="oauth2Login">
<p>Or login with:</p>
<a href="/authorize?provider=google&redirect_to=/web/login">Google</a>
</div>
<script>
document
.getElementById("submitBtn")
@ -55,6 +60,31 @@
).innerText = `Login failed: ${error.message}`;
});
});
if (window.location.hash) {
// OAuth
// Extract data from the URL fragment
const fragmentData = window.location.hash.substring(1); // Remove the leading #
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token
fetch(`/web-api/login_refresh/${refreshToken}`, {
// Login in via refresh_token
method: "POST",
})
.then((response) => {
if (!response.ok) {
// If HTTP status code is not OK, throw an error with the status text
throw Error(response.statusText);
}
window.location.href = "/web/home";
})
.catch((error) => {
console.error(`Error:, ${error}`);
document.getElementById(
"response",
).innerText = `OAuth Login failed: ${error.message}`;
});
}
</script>
</body>
</html>

4
doc/README.md Normal file
View File

@ -0,0 +1,4 @@
# Docs
- Directory to contain information about usage and development.
- [Appflowy Cloud Deployment](./deployment.md)
- [Appflowy with Cloud](./integration.md)

120
doc/deployment.md Normal file
View File

@ -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 `<your host server public ip/hostname>/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)

1
doc/integration.md Normal file
View File

@ -0,0 +1 @@
# Using AppFlowy with AppFlowy Cloud

View File

@ -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}

View File

@ -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}

View File

@ -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"]

View File

@ -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::<i64>().context("parser expires_in failed")?);
},
"expires_at" => {
expires_at = Some(v.parse::<i64>().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::<i64>().context("parser expires_in failed")?),
"expires_at" => expires_at = Some(v.parse::<i64>().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<String, anyhow::Error> {
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)
}

View File

@ -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,

View File

@ -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;
}
}