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:
parent
88be0c2433
commit
d638c01763
47
README.md
47
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# Docs
|
||||
- Directory to contain information about usage and development.
|
||||
- [Appflowy Cloud Deployment](./deployment.md)
|
||||
- [Appflowy with Cloud](./integration.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 `<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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Using AppFlowy with AppFlowy Cloud
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue