Merge pull request #310 from AppFlowy-IO/admin-frontend/self-host-env
Admin frontend/self host env
This commit is contained in:
commit
e1ac591ea0
|
|
@ -117,10 +117,13 @@ pub async fn invite_handler(
|
||||||
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
||||||
state
|
state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.magic_link(&MagicLinkParams {
|
.magic_link(
|
||||||
email: param.email,
|
&MagicLinkParams {
|
||||||
..Default::default()
|
email: param.email,
|
||||||
})
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some("/".to_owned()),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(WebApiResponse::<()>::from_str("Invitation sent".into()))
|
Ok(WebApiResponse::<()>::from_str("Invitation sent".into()))
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +311,7 @@ pub async fn sign_up_handler(
|
||||||
|
|
||||||
let sign_up_res = state
|
let sign_up_res = state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.sign_up_with_referrer(¶m.email, ¶m.password, Some("/"))
|
.sign_up(¶m.email, ¶m.password, Some("/"))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match sign_up_res {
|
match sign_up_res {
|
||||||
|
|
@ -378,10 +381,13 @@ async fn send_magic_link(
|
||||||
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
||||||
state
|
state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.magic_link(&MagicLinkParams {
|
.magic_link(
|
||||||
email: email.to_owned(),
|
&MagicLinkParams {
|
||||||
..Default::default()
|
email: email.to_owned(),
|
||||||
})
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some("/".to_owned()),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(WebApiResponse::<()>::from_str("Magic Link Sent".into()))
|
Ok(WebApiResponse::<()>::from_str("Magic Link Sent".into()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
deploy.env
17
deploy.env
|
|
@ -1,11 +1,18 @@
|
||||||
# GoTrue URL that the appflowy service will use to connect to gotrue
|
|
||||||
# In docker environment, `gotrue` is the hostname of the gotrue service
|
|
||||||
APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999
|
|
||||||
APPFLOWY_DATABASE_URL=postgres://postgres:password@postgres:5432/postgres
|
|
||||||
|
|
||||||
# This file is a template for docker compose deployment
|
# This file is a template for docker compose deployment
|
||||||
# Copy this file to .env and change the values as needed
|
# Copy this file to .env and change the values as needed
|
||||||
|
|
||||||
|
# AppFlowy Cloud
|
||||||
|
## URL that connects to the gotrue docker container
|
||||||
|
APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999
|
||||||
|
## URL that connects to the postgres docker container
|
||||||
|
APPFLOWY_DATABASE_URL=postgres://postgres:password@postgres:5432/postgres
|
||||||
|
|
||||||
|
# admin frontend
|
||||||
|
## URL that connects to redis docker container
|
||||||
|
ADMIN_FRONTEND_REDIS_URL=redis://redis:6379
|
||||||
|
## URL that connects to gotrue docker container
|
||||||
|
ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999
|
||||||
|
|
||||||
# authentication key, change this and keep the key safe and secret
|
# authentication key, change this and keep the key safe and secret
|
||||||
# self defined key, you can use any string
|
# self defined key, you can use any string
|
||||||
GOTRUE_JWT_SECRET=hello456
|
GOTRUE_JWT_SECRET=hello456
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./admin_frontend/Dockerfile
|
dockerfile: ./admin_frontend/Dockerfile
|
||||||
image: appflowyinc/admin_frontend:${BACKEND_VERSION:-latest}
|
image: appflowyinc/admin_frontend:${BACKEND_VERSION:-latest}
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=${RUST_LOG:-info}
|
||||||
|
- ADMIN_FRONTEND_REDIS_URL=${ADMIN_FRONTEND_REDIS_URL:-redis://redis:6379}
|
||||||
|
- ADMIN_FRONTEND_GOTRUE_URL=${ADMIN_FRONTEND_GOTRUE_URL:-http://gotrue:9999}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
|
||||||
|
|
@ -373,10 +373,13 @@ impl Client {
|
||||||
pub async fn invite(&self, email: &str) -> Result<(), AppResponseError> {
|
pub async fn invite(&self, email: &str) -> Result<(), AppResponseError> {
|
||||||
self
|
self
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.magic_link(&MagicLinkParams {
|
.magic_link(
|
||||||
email: email.to_owned(),
|
&MagicLinkParams {
|
||||||
..Default::default()
|
email: email.to_owned(),
|
||||||
})
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -676,7 +679,7 @@ impl Client {
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppResponseError> {
|
pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppResponseError> {
|
||||||
match self.gotrue_client.sign_up(email, password).await? {
|
match self.gotrue_client.sign_up(email, password, None).await? {
|
||||||
Authenticated(access_token_resp) => {
|
Authenticated(access_token_resp) => {
|
||||||
self.token.write().set(access_token_resp);
|
self.token.write().set(access_token_resp);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResponse, GoTrueError> {
|
pub async fn sign_up(
|
||||||
self.sign_up_with_referrer(email, password, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
|
||||||
pub async fn sign_up_with_referrer(
|
|
||||||
&self,
|
&self,
|
||||||
email: &str,
|
email: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
|
|
@ -224,14 +219,17 @@ impl Client {
|
||||||
to_gotrue_result(resp).await
|
to_gotrue_result(resp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn magic_link(&self, magic_link_params: &MagicLinkParams) -> Result<(), GoTrueError> {
|
pub async fn magic_link(
|
||||||
|
&self,
|
||||||
|
magic_link_params: &MagicLinkParams,
|
||||||
|
redirect_to: Option<String>,
|
||||||
|
) -> Result<(), GoTrueError> {
|
||||||
let url = format!("{}/magiclink", self.base_url);
|
let url = format!("{}/magiclink", self.base_url);
|
||||||
let resp = self
|
let mut req_builder = self.client.request(Method::POST, &url);
|
||||||
.client
|
if let Some(redirect_to) = redirect_to {
|
||||||
.request(Method::POST, &url)
|
req_builder = req_builder.header("redirect_to", redirect_to);
|
||||||
.json(&magic_link_params)
|
}
|
||||||
.send()
|
let resp = req_builder.json(&magic_link_params).send().await?;
|
||||||
.await?;
|
|
||||||
check_gotrue_result(resp).await
|
check_gotrue_result(resp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ async fn setup_admin_account(
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let admin_email = gotrue_setting.admin_email.as_str();
|
let admin_email = gotrue_setting.admin_email.as_str();
|
||||||
let password = gotrue_setting.admin_password.as_str();
|
let password = gotrue_setting.admin_password.as_str();
|
||||||
let res_resp = gotrue_client.sign_up(admin_email, password).await;
|
let res_resp = gotrue_client.sign_up(admin_email, password, None).await;
|
||||||
match res_resp {
|
match res_resp {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if let app_error::gotrue::GoTrueError::Internal(err) = err {
|
if let app_error::gotrue::GoTrueError::Internal(err) = err {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
|
#[allow(clippy::blocks_in_if_conditions)]
|
||||||
async fn check_collab_permission(
|
async fn check_collab_permission(
|
||||||
&self,
|
&self,
|
||||||
oid: &str,
|
oid: &str,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,60 @@ where
|
||||||
opened_collab_by_object_id: Arc::new(RwLock::new(HashMap::new())),
|
opened_collab_by_object_id: Arc::new(RwLock::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_collab_permission(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
uid: &i64,
|
||||||
|
params: &CollabParams,
|
||||||
|
transaction: &mut Transaction<'_, sqlx::Postgres>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
// Check if the user has enough permissions to insert collab
|
||||||
|
// 1. If the collab already exists, check if the user has enough permissions to update collab
|
||||||
|
// 2. If the collab doesn't exist, check if the user has enough permissions to create collab.
|
||||||
|
let collab_exists = is_collab_exists(¶ms.object_id, transaction.deref_mut()).await?;
|
||||||
|
if collab_exists {
|
||||||
|
// If the collab already exists, check if the user has enough permissions to update collab
|
||||||
|
let can_write = self
|
||||||
|
.access_control
|
||||||
|
.get_or_refresh_collab_access_level(uid, ¶ms.object_id, transaction.deref_mut())
|
||||||
|
.await
|
||||||
|
.context(format!(
|
||||||
|
"Can't find the access level when user:{} try to insert collab",
|
||||||
|
uid
|
||||||
|
))?
|
||||||
|
.can_write();
|
||||||
|
if !can_write {
|
||||||
|
return Err(AppError::NotEnoughPermissions(format!(
|
||||||
|
"user:{} doesn't have enough permissions to update collab {}",
|
||||||
|
uid, params.object_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the collab doesn't exist, check if the user has enough permissions to create collab.
|
||||||
|
// If the user is the owner or member of the workspace, the user can create collab.
|
||||||
|
let can_write_workspace = self
|
||||||
|
.access_control
|
||||||
|
.get_user_workspace_role(uid, workspace_id, transaction.deref_mut())
|
||||||
|
.await?
|
||||||
|
.can_create_collab();
|
||||||
|
|
||||||
|
if !can_write_workspace {
|
||||||
|
return Err(AppError::NotEnoughPermissions(format!(
|
||||||
|
"user:{} doesn't have enough permissions to insert collab {}",
|
||||||
|
uid, params.object_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the access level if the user has enough permissions to create collab.
|
||||||
|
self
|
||||||
|
.access_control
|
||||||
|
.cache_collab_access_level(uid, ¶ms.object_id, AFAccessLevel::FullAccess)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -128,6 +182,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip(self, params), oid = %params.oid, err)]
|
#[instrument(level = "trace", skip(self, params), oid = %params.oid, err)]
|
||||||
|
#[allow(clippy::blocks_in_if_conditions)]
|
||||||
async fn upsert_collab_with_transaction(
|
async fn upsert_collab_with_transaction(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
|
|
@ -136,46 +191,9 @@ where
|
||||||
transaction: &mut Transaction<'_, sqlx::Postgres>,
|
transaction: &mut Transaction<'_, sqlx::Postgres>,
|
||||||
) -> DatabaseResult<()> {
|
) -> DatabaseResult<()> {
|
||||||
params.validate()?;
|
params.validate()?;
|
||||||
|
self
|
||||||
// Check if the user has enough permissions to insert collab
|
.check_collab_permission(workspace_id, uid, ¶ms, transaction)
|
||||||
// 1. If the collab already exists, check if the user has enough permissions to update collab
|
.await?;
|
||||||
// 2. If the collab doesn't exist, check if the user has enough permissions to create collab.
|
|
||||||
let has_permission = if is_collab_exists(¶ms.object_id, transaction.deref_mut()).await? {
|
|
||||||
// If the collab already exists, check if the user has enough permissions to update collab
|
|
||||||
let level = self
|
|
||||||
.access_control
|
|
||||||
.get_or_refresh_collab_access_level(uid, ¶ms.object_id, transaction.deref_mut())
|
|
||||||
.await
|
|
||||||
.context(format!(
|
|
||||||
"Can't find the access level when user:{} try to insert collab",
|
|
||||||
uid
|
|
||||||
))?;
|
|
||||||
level.can_write()
|
|
||||||
} else {
|
|
||||||
// If the collab doesn't exist, check if the user has enough permissions to create collab.
|
|
||||||
// If the user is the owner or member of the workspace, the user can create collab.
|
|
||||||
let can_write_workspace = self
|
|
||||||
.access_control
|
|
||||||
.get_user_workspace_role(uid, workspace_id, transaction.deref_mut())
|
|
||||||
.await?
|
|
||||||
.can_create_collab();
|
|
||||||
|
|
||||||
// Cache the access level if the user has enough permissions to create collab.
|
|
||||||
if can_write_workspace {
|
|
||||||
self
|
|
||||||
.access_control
|
|
||||||
.cache_collab_access_level(uid, ¶ms.object_id, AFAccessLevel::FullAccess)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
can_write_workspace
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
|
||||||
return Err(AppError::NotEnoughPermissions(format!(
|
|
||||||
"user:{} doesn't have enough permissions to insert collab {}",
|
|
||||||
uid, params.object_id
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let object_id = params.object_id.clone();
|
let object_id = params.object_id.clone();
|
||||||
let encoded_collab = params.encoded_collab_v1.clone();
|
let encoded_collab = params.encoded_collab_v1.clone();
|
||||||
self
|
self
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all, err)]
|
#[instrument(level = "trace", skip_all, err)]
|
||||||
|
#[allow(clippy::blocks_in_if_conditions)]
|
||||||
async fn check_workspace_permission(
|
async fn check_workspace_permission(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &Uuid,
|
workspace_id: &Uuid,
|
||||||
|
|
@ -208,29 +209,28 @@ where
|
||||||
method: Method,
|
method: Method,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
trace!("workspace_id: {:?}, uid: {:?}", workspace_id, uid);
|
trace!("workspace_id: {:?}, uid: {:?}", workspace_id, uid);
|
||||||
match self
|
let role = self
|
||||||
.access_control
|
.access_control
|
||||||
.get_role_from_uid(uid, workspace_id, &self.pg_pool)
|
.get_role_from_uid(uid, workspace_id, &self.pg_pool)
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|err| {
|
||||||
Ok(role) => {
|
AppError::NotEnoughPermissions(format!(
|
||||||
if method == Method::DELETE || method == Method::POST || method == Method::PUT {
|
"Can't find the role of the user:{:?} in the workspace:{:?}. error: {}",
|
||||||
if matches!(role, AFRole::Owner) {
|
uid, workspace_id, err
|
||||||
Ok(())
|
))
|
||||||
} else {
|
})?;
|
||||||
Err(AppError::NotEnoughPermissions(format!(
|
|
||||||
"User:{:?} doesn't have the enough permission to access workspace:{}",
|
match method {
|
||||||
uid, workspace_id
|
Method::DELETE | Method::POST | Method::PUT => match role {
|
||||||
)))
|
AFRole::Owner => return Ok(()),
|
||||||
}
|
_ => {
|
||||||
} else {
|
return Err(AppError::NotEnoughPermissions(format!(
|
||||||
Ok(())
|
"User:{:?} doesn't have the enough permission to access workspace:{}",
|
||||||
}
|
uid, workspace_id
|
||||||
|
)))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Err(err) => Err(AppError::NotEnoughPermissions(format!(
|
_ => Ok(()),
|
||||||
"Can't find the role of the user:{:?} in the workspace:{:?}. error: {}",
|
|
||||||
uid, workspace_id, err
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,12 +175,13 @@ where
|
||||||
forward_ready!(service);
|
forward_ready!(service);
|
||||||
|
|
||||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||||
match req.match_pattern().map(|pattern| {
|
let path = req.match_pattern().map(|pattern| {
|
||||||
let resource_ref = ResourceDef::new(pattern);
|
let resource_ref = ResourceDef::new(pattern);
|
||||||
let mut path = req.match_info().clone();
|
let mut path = req.match_info().clone();
|
||||||
resource_ref.capture_match_info(&mut path);
|
resource_ref.capture_match_info(&mut path);
|
||||||
path
|
path
|
||||||
}) {
|
});
|
||||||
|
match path {
|
||||||
None => {
|
None => {
|
||||||
let fut = self.service.call(req);
|
let fut = self.service.call(req);
|
||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue