mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
feat: issue#1173 allow admin frontend to be served on a different path
This commit is contained in:
parent
b7e08687b4
commit
a352b36eb9
37 changed files with 312 additions and 167 deletions
|
@ -8,6 +8,7 @@ pub struct Config {
|
|||
pub gotrue_url: String,
|
||||
pub appflowy_cloud_url: String,
|
||||
pub oauth: OAuthConfig,
|
||||
pub path_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -41,6 +42,7 @@ impl Config {
|
|||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
},
|
||||
path_prefix: get_or_default("ADMIN_FRONTEND_PATH_PREFIX", ""),
|
||||
};
|
||||
Ok(cfg)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ impl From<redis::RedisError> for WebApiError<'_> {
|
|||
|
||||
pub enum WebAppError {
|
||||
Askama(askama::Error),
|
||||
GoTrue(gotrue_entity::error::GoTrueError),
|
||||
LoginRedirectRequired(String),
|
||||
ExtApi(ext::error::Error),
|
||||
Redis(redis::RedisError),
|
||||
BadRequest(String),
|
||||
|
@ -59,9 +59,8 @@ impl IntoResponse for WebAppError {
|
|||
tracing::error!("askama error: {:?}", e);
|
||||
status::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
},
|
||||
WebAppError::GoTrue(e) => {
|
||||
tracing::error!("gotrue error: {:?}", e);
|
||||
Redirect::to("/login").into_response()
|
||||
WebAppError::LoginRedirectRequired(base_path) => {
|
||||
Redirect::to(&format!("{}/login", base_path)).into_response()
|
||||
},
|
||||
WebAppError::ExtApi(e) => e.into_response(),
|
||||
WebAppError::Redis(e) => {
|
||||
|
@ -88,12 +87,6 @@ impl From<askama::Error> for WebAppError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<gotrue_entity::error::GoTrueError> for WebAppError {
|
||||
fn from(v: gotrue_entity::error::GoTrueError) -> Self {
|
||||
WebAppError::GoTrue(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ext::error::Error> for WebAppError {
|
||||
fn from(v: ext::error::Error) -> Self {
|
||||
WebAppError::ExtApi(v)
|
||||
|
|
|
@ -49,6 +49,7 @@ async fn main() {
|
|||
let session_store = session::SessionStorage::new(redis_client);
|
||||
|
||||
let address = format!("{}:{}", config.host, config.port);
|
||||
let path_prefix = config.path_prefix.clone();
|
||||
let state = AppState {
|
||||
appflowy_cloud_url: config.appflowy_cloud_url.clone(),
|
||||
gotrue_client,
|
||||
|
@ -57,17 +58,33 @@ async fn main() {
|
|||
};
|
||||
|
||||
let web_app_router = web_app::router(state.clone()).with_state(state.clone());
|
||||
let web_api_router = web_api::router().with_state(state);
|
||||
let web_api_router = web_api::router().with_state(state.clone());
|
||||
|
||||
let app = Router::new()
|
||||
let favicon_redirect_url = state.prepend_with_path_prefix("/assets/favicon.ico");
|
||||
let base_path_redirect_url = state.prepend_with_path_prefix("/web");
|
||||
let base_app = Router::new()
|
||||
.route(
|
||||
"/favicon.ico",
|
||||
get(|| async { Redirect::permanent("/assets/favicon.ico") }),
|
||||
get(|| async {
|
||||
let favicon_redirect_url = favicon_redirect_url;
|
||||
Redirect::permanent(&favicon_redirect_url)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/",
|
||||
get(|| async {
|
||||
let base_path_redirect_url = base_path_redirect_url;
|
||||
Redirect::permanent(&base_path_redirect_url)
|
||||
}),
|
||||
)
|
||||
.route("/", get(|| async { Redirect::permanent("/web") }))
|
||||
.nest_service("/web", web_app_router)
|
||||
.nest_service("/web-api", web_api_router)
|
||||
.nest_service("/assets", ServeDir::new("assets"));
|
||||
let app = if path_prefix.is_empty() {
|
||||
base_app
|
||||
} else {
|
||||
Router::new().nest(&path_prefix, base_app)
|
||||
};
|
||||
|
||||
let listener = TcpListener::bind(address)
|
||||
.await
|
||||
|
|
|
@ -10,6 +10,12 @@ pub struct AppState {
|
|||
pub config: Config,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn prepend_with_path_prefix(&self, path: &str) -> String {
|
||||
format!("{}{}", self.config.path_prefix, path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WebApiLoginRequest {
|
||||
pub email: String,
|
||||
|
|
|
@ -140,7 +140,8 @@ impl FromRequestParts<AppState> for UserSession {
|
|||
Ok(jar) => jar,
|
||||
Err(err) => {
|
||||
tracing::error!("failed to get cookie jar, error: {}", err);
|
||||
return Err(Redirect::to("/web/login").into_response());
|
||||
let redirect_url = state.prepend_with_path_prefix("/web/login");
|
||||
return Err(Redirect::to(&redirect_url).into_response());
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -156,8 +157,15 @@ impl FromRequestParts<AppState> for UserSession {
|
|||
.map(|uri| urlencoding::encode(&uri.to_string()).to_string());
|
||||
|
||||
match original_url {
|
||||
Some(url) => Err(Redirect::to(&format!("/web/login-v2?redirect_to={}", url)).into_response()),
|
||||
None => Err(Redirect::to("/web/login").into_response()),
|
||||
Some(url) => {
|
||||
let redirect_url =
|
||||
state.prepend_with_path_prefix(&format!("/web/login-v2?redirect_to={}", url));
|
||||
Err(Redirect::to(&redirect_url).into_response())
|
||||
},
|
||||
None => {
|
||||
let redirect_url = state.prepend_with_path_prefix("/web/login");
|
||||
Err(Redirect::to(&redirect_url).into_response())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,9 +60,10 @@ pub struct ChangePassword;
|
|||
#[derive(Template)]
|
||||
#[template(path = "pages/login.html")]
|
||||
pub struct Login<'a> {
|
||||
pub path_prefix: &'a str,
|
||||
pub oauth_providers: &'a [&'a str],
|
||||
pub redirect_to: Option<&'a str>,
|
||||
pub oauth_redirect_to: Option<&'a str>,
|
||||
pub oauth_redirect_to: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -70,7 +71,8 @@ pub struct Login<'a> {
|
|||
pub struct LoginV2<'a> {
|
||||
pub oauth_providers: &'a [&'a str],
|
||||
pub redirect_to: Option<&'a str>,
|
||||
pub oauth_redirect_to: Option<&'a str>,
|
||||
pub oauth_redirect_to: &'a str,
|
||||
pub path_prefix: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -78,6 +80,7 @@ pub struct LoginV2<'a> {
|
|||
pub struct Home<'a> {
|
||||
pub user: &'a User,
|
||||
pub is_admin: bool,
|
||||
pub path_prefix: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -109,6 +112,7 @@ pub struct Navigate;
|
|||
#[derive(Template)]
|
||||
#[template(path = "pages/admin_home.html")]
|
||||
pub struct AdminHome<'a> {
|
||||
pub path_prefix: &'a str,
|
||||
pub user: &'a User,
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,8 @@ async fn delete_account_handler(
|
|||
session: UserSession,
|
||||
) -> Result<axum::response::Response, WebApiError<'static>> {
|
||||
delete_current_user(&session.token.access_token, &state.appflowy_cloud_url).await?;
|
||||
Ok(Redirect::to("/web/login").into_response())
|
||||
let redirect_url = state.prepend_with_path_prefix("/web/login");
|
||||
Ok(Redirect::to(&redirect_url).into_response())
|
||||
}
|
||||
|
||||
// Invite another user, this will trigger email sending
|
||||
|
@ -142,6 +143,11 @@ async fn invite_handler(
|
|||
State(state): State<AppState>,
|
||||
Form(param): Form<WebApiInviteUserRequest>,
|
||||
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
||||
let magic_link_redirect = if state.config.path_prefix.is_empty() {
|
||||
"/".to_owned()
|
||||
} else {
|
||||
state.config.path_prefix.clone()
|
||||
};
|
||||
state
|
||||
.gotrue_client
|
||||
.magic_link(
|
||||
|
@ -149,7 +155,7 @@ async fn invite_handler(
|
|||
email: param.email,
|
||||
..Default::default()
|
||||
},
|
||||
Some("/".to_owned()),
|
||||
Some(magic_link_redirect),
|
||||
)
|
||||
.await?;
|
||||
Ok(WebApiResponse::<()>::from_str("Invitation sent".into()))
|
||||
|
@ -558,10 +564,11 @@ async fn logout_handler(
|
|||
.value();
|
||||
|
||||
state.session_store.del_user_session(session_id).await?;
|
||||
let htmx_redirect_url = format!("{}/web/login", state.config.path_prefix);
|
||||
Ok(
|
||||
(
|
||||
jar.remove(Cookie::from("session_id")),
|
||||
htmx_redirect("/web/login"),
|
||||
htmx_redirect(&htmx_redirect_url),
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
|
@ -599,11 +606,15 @@ async fn session_login(
|
|||
None
|
||||
},
|
||||
});
|
||||
|
||||
let default_htmx_redirect_url = format!("{}/web/home", state.config.path_prefix);
|
||||
Ok(
|
||||
(
|
||||
jar.add(new_session_cookie(new_session_id)),
|
||||
htmx_redirect(decoded_redirect_to.as_deref().unwrap_or("/web/home")),
|
||||
htmx_redirect(
|
||||
decoded_redirect_to
|
||||
.as_deref()
|
||||
.unwrap_or(&default_htmx_redirect_url),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
|
@ -620,7 +631,7 @@ async fn send_magic_link(
|
|||
email: email.to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
Some("/web/login-callback".to_owned()),
|
||||
Some(format!("{}/web/login-callback", state.config.path_prefix)),
|
||||
)
|
||||
.await?;
|
||||
Ok(WebApiResponse::<()>::from_str("Magic Link Sent".into()))
|
||||
|
|
|
@ -17,6 +17,8 @@ use gotrue_entity::dto::User;
|
|||
|
||||
use crate::{templates, AppState};
|
||||
|
||||
static DEFAULT_OAUTH_REDIRECT_TO_WITHOUT_PREFIX: &str = "/web/login-callback";
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest_service("/", page_router().with_state(state.clone()))
|
||||
|
@ -118,7 +120,8 @@ async fn login_callback_query_handler(
|
|||
.token(&gotrue::grant::Grant::RefreshToken(
|
||||
gotrue::grant::RefreshTokenGrant { refresh_token },
|
||||
))
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
|
||||
verify_token_cloud(
|
||||
token.access_token.as_str(),
|
||||
|
@ -205,7 +208,8 @@ async fn admin_sso_detail_handler(
|
|||
let sso_provider = state
|
||||
.gotrue_client
|
||||
.admin_get_sso_provider(&session.token.access_token, &sso_provider_id)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
|
||||
let mapping_json =
|
||||
serde_json::to_string_pretty(&sso_provider.saml.attribute_mapping).unwrap_or("".to_owned());
|
||||
|
@ -227,7 +231,8 @@ async fn admin_sso_handler(
|
|||
let sso_providers = state
|
||||
.gotrue_client
|
||||
.admin_list_sso_providers(&session.token.access_token)
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?
|
||||
.items
|
||||
.unwrap_or_default();
|
||||
|
||||
|
@ -354,7 +359,8 @@ async fn user_user_handler(
|
|||
let user = state
|
||||
.gotrue_client
|
||||
.user_info(&session.token.access_token)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
render_template(templates::UserDetails { user: &user })
|
||||
}
|
||||
|
||||
|
@ -368,38 +374,62 @@ async fn login_handler(
|
|||
.map(|r| urlencoding::encode(r).to_string());
|
||||
let oauth_redirect_to = login.redirect_to.as_ref().map(|r| {
|
||||
urlencoding::encode(&format!(
|
||||
"/web/login-callback?redirect_to={}",
|
||||
"{}/web/login-callback?redirect_to={}",
|
||||
state.config.path_prefix,
|
||||
urlencoding::encode(r)
|
||||
))
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let external = state.gotrue_client.settings().await?.external;
|
||||
let external = state
|
||||
.gotrue_client
|
||||
.settings()
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?
|
||||
.external;
|
||||
let oauth_providers = external.oauth_providers();
|
||||
let default_oauth_redirect_to = format!(
|
||||
"{}{}",
|
||||
state.config.path_prefix, DEFAULT_OAUTH_REDIRECT_TO_WITHOUT_PREFIX
|
||||
);
|
||||
render_template(templates::Login {
|
||||
path_prefix: &state.config.path_prefix,
|
||||
oauth_providers: &oauth_providers,
|
||||
redirect_to: redirect_to.as_deref(),
|
||||
oauth_redirect_to: oauth_redirect_to.as_deref(),
|
||||
oauth_redirect_to: oauth_redirect_to
|
||||
.as_deref()
|
||||
.unwrap_or(&default_oauth_redirect_to),
|
||||
})
|
||||
}
|
||||
|
||||
async fn login_v2_handler(Query(login): Query<LoginParams>) -> Result<Html<String>, WebAppError> {
|
||||
async fn login_v2_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(login): Query<LoginParams>,
|
||||
) -> Result<Html<String>, WebAppError> {
|
||||
let redirect_to = login
|
||||
.redirect_to
|
||||
.as_ref()
|
||||
.map(|r| urlencoding::encode(r).to_string());
|
||||
let oauth_redirect_to = login.redirect_to.as_ref().map(|r| {
|
||||
urlencoding::encode(&format!(
|
||||
"/web/login-callback?redirect_to={}",
|
||||
"{}/web/login-callback?redirect_to={}",
|
||||
state.config.path_prefix,
|
||||
urlencoding::encode(r)
|
||||
))
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let default_oauth_redirect_to = format!(
|
||||
"{}{}",
|
||||
state.config.path_prefix, DEFAULT_OAUTH_REDIRECT_TO_WITHOUT_PREFIX
|
||||
);
|
||||
render_template(templates::LoginV2 {
|
||||
oauth_providers: &["Google", "Apple", "Github", "Discord"],
|
||||
redirect_to: redirect_to.as_deref(),
|
||||
oauth_redirect_to: oauth_redirect_to.as_deref(),
|
||||
oauth_redirect_to: oauth_redirect_to
|
||||
.as_deref()
|
||||
.unwrap_or(&default_oauth_redirect_to),
|
||||
path_prefix: &state.config.path_prefix,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -412,18 +442,21 @@ pub async fn home_handler(
|
|||
session: Option<UserSession>,
|
||||
jar: CookieJar,
|
||||
) -> Result<axum::response::Response, WebAppError> {
|
||||
let redirect_url = state.prepend_with_path_prefix("/web/login");
|
||||
let session = match session {
|
||||
Some(session) => session,
|
||||
None => return Ok(Redirect::to("/web/login").into_response()),
|
||||
None => return Ok(Redirect::to(&redirect_url).into_response()),
|
||||
};
|
||||
|
||||
let user = state
|
||||
.gotrue_client
|
||||
.user_info(&session.token.access_token)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
let home_html_str = render_template(templates::Home {
|
||||
user: &user,
|
||||
is_admin: is_admin(&user),
|
||||
path_prefix: &state.config.path_prefix,
|
||||
})?;
|
||||
Ok((jar, home_html_str).into_response())
|
||||
}
|
||||
|
@ -435,8 +468,12 @@ async fn admin_home_handler(
|
|||
let user = state
|
||||
.gotrue_client
|
||||
.user_info(&session.token.access_token)
|
||||
.await?;
|
||||
render_template(templates::AdminHome { user: &user })
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
render_template(templates::AdminHome {
|
||||
user: &user,
|
||||
path_prefix: &state.config.path_prefix,
|
||||
})
|
||||
}
|
||||
|
||||
async fn admin_users_handler(
|
||||
|
@ -469,7 +506,8 @@ async fn admin_user_details_handler(
|
|||
let user = state
|
||||
.gotrue_client
|
||||
.admin_user_details(&session.token.access_token, &user_id)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| WebAppError::LoginRedirectRequired(state.config.path_prefix.clone()))?;
|
||||
|
||||
render_template(templates::AdminUserDetails { user: &user })
|
||||
}
|
||||
|
|
|
@ -4,35 +4,35 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/admin/navigate"
|
||||
hx-get="../../web/components/admin/navigate"
|
||||
>
|
||||
Navigate
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/admin/users"
|
||||
hx-get="../../web/components/admin/users"
|
||||
>
|
||||
List Users
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/admin/users/create"
|
||||
hx-get="../../web/components/admin/users/create"
|
||||
>
|
||||
Create User
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/admin/sso"
|
||||
hx-get="../../web/components/admin/sso"
|
||||
>
|
||||
List SSO
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/admin/sso/create"
|
||||
hx-get="../../web/components/admin/sso/create"
|
||||
>
|
||||
Create SSO
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<h4>Please enter the following information to create new SSO</h4>
|
||||
<form hx-post="/web-api/admin/sso" hx-target="#none">
|
||||
<form hx-post="../../web-api/admin/sso" hx-target="#none">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Email</td>
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
<button
|
||||
class="button cyan"
|
||||
hx-target="#sso-list"
|
||||
hx-get="/web/components/admin/sso/{{ sso_provider.id|escape }}"
|
||||
hx-get="../../web/components/admin/sso/{{ sso_provider.id|escape }}"
|
||||
>
|
||||
More Info
|
||||
</button>
|
||||
<button
|
||||
class="deleteUserBtn button red"
|
||||
hx-delete="/web-api/admin/sso/{{ sso_provider.id|escape }}"
|
||||
hx-delete="../../web-api/admin/sso/{{ sso_provider.id|escape }}"
|
||||
hx-confirm="Are you sure?"
|
||||
hx-target="closest tr"
|
||||
hx-swap="delete"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<div
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/user"
|
||||
hx-get="../../web/components/user/user"
|
||||
class="button red"
|
||||
>
|
||||
{{ user.email|escape }}
|
||||
|
@ -18,11 +18,11 @@
|
|||
document
|
||||
.getElementById("adminBtn")
|
||||
.addEventListener("click", function () {
|
||||
window.location.href = "/web/home";
|
||||
window.location.href = "{{ path_prefix }}/web/home";
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="button yellow" id="logoutBtn" hx-post="/web-api/logout">
|
||||
<div class="button yellow" id="logoutBtn" hx-post="../../web-api/logout">
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
{% include "user_details.html" %}
|
||||
|
||||
<div>
|
||||
<form hx-put="/web-api/admin/user/{{ user.id|escape }}" hx-target="#none">
|
||||
<form
|
||||
hx-put="../../web-api/admin/user/{{ user.id|escape }}"
|
||||
hx-target="#none"
|
||||
>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Set Password:</td>
|
||||
|
@ -30,7 +33,7 @@
|
|||
<td>
|
||||
<button
|
||||
class="button cyan"
|
||||
hx-post="/web-api/admin/user/{{ user.email|escape }}/generate-link"
|
||||
hx-post="../../web-api/admin/user/{{ user.email|escape }}/generate-link"
|
||||
hx-target="#inviteLink"
|
||||
hx-trigger="click"
|
||||
>
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
<button
|
||||
class="button cyan"
|
||||
hx-target="#admin-users"
|
||||
hx-get="/web/components/admin/users/{{ user.id|escape }}"
|
||||
hx-get="../../web/components/admin/users/{{ user.id|escape }}"
|
||||
>
|
||||
More Info
|
||||
</button>
|
||||
<button
|
||||
class="deleteUserBtn button red"
|
||||
hx-delete="/web-api/admin/user/{{ user.id|escape }}"
|
||||
hx-delete="../../web-api/admin/user/{{ user.id|escape }}"
|
||||
hx-confirm="Are you sure?"
|
||||
hx-target="closest tr"
|
||||
hx-swap="delete"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<h3>Password Change</h3>
|
||||
<form hx-post="/web-api/change-password" hx-target="#none">
|
||||
<form hx-post="../web-api/change-password" hx-target="#none">
|
||||
<table>
|
||||
<tr>
|
||||
<td>New Password:</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div id="create-user">
|
||||
<h4>Please enter the following information to create a new user</h4>
|
||||
<form hx-post="/web-api/admin/user" hx-target="#none">
|
||||
<form hx-post="../../web-api/admin/user" hx-target="#none">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Email:</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div id="invite-user">
|
||||
<h4>Invite another user to AppFlowy</h4>
|
||||
<form hx-post="/web-api/invite" hx-target="#none">
|
||||
<form hx-post="../web-api/invite" hx-target="#none">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Email:</td>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<br />
|
||||
<h4>Workspaces shared with you</h4>
|
||||
<div
|
||||
hx-get="/web/components/user/shared-workspaces"
|
||||
hx-get="../web/components/user/shared-workspaces"
|
||||
hx-trigger="workspaceInvitationAccepted from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
|
@ -35,23 +35,26 @@
|
|||
<br />
|
||||
<h4>Invite another user to your workspace</h4>
|
||||
<table class="red-table table">
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Members</th>
|
||||
<th>Invite</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for owned_workspace in owned_workspaces %}
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Members</th>
|
||||
<th>Invite</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for owned_workspace in owned_workspaces %}
|
||||
<tr>
|
||||
<td> {{ owned_workspace.workspace.workspace_name|escape }} </td>
|
||||
<td>{{ owned_workspace.workspace.workspace_name|escape }}</td>
|
||||
<td>
|
||||
{% for member in owned_workspace.members %}
|
||||
{{ member.email|escape }} <br />
|
||||
{% for member in owned_workspace.members %} {{ member.email|escape }}
|
||||
<br />
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<form hx-post="/web-api/workspace/{{ owned_workspace.workspace.workspace_id|escape }}/invite" hx-target="#none">
|
||||
<form
|
||||
hx-post="../web-api/workspace/{{ owned_workspace.workspace.workspace_id|escape }}/invite"
|
||||
hx-target="#none"
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
name="email"
|
||||
|
@ -62,26 +65,30 @@
|
|||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<br />
|
||||
<h4>Invitation(s) from other user(s)</h4>
|
||||
<table class="purple-table table">
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Inviter</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for pending_workspace_invitation in pending_workspace_invitations %}
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Inviter</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for pending_workspace_invitation in pending_workspace_invitations %}
|
||||
<tr>
|
||||
<td> {{ pending_workspace_invitation.workspace_name|default("")|escape }} </td>
|
||||
<td> {{ pending_workspace_invitation.inviter_email|default("")|escape }} </td>
|
||||
<td>
|
||||
{{ pending_workspace_invitation.workspace_name|default("")|escape }}
|
||||
</td>
|
||||
<td>
|
||||
{{ pending_workspace_invitation.inviter_email|default("")|escape }}
|
||||
</td>
|
||||
<td>
|
||||
<form
|
||||
hx-post="/web-api/invite/{{ pending_workspace_invitation.invite_id|escape }}/accept"
|
||||
hx-post="../web-api/invite/{{ pending_workspace_invitation.invite_id|escape }}/accept"
|
||||
hx-target="closest tr"
|
||||
hx-swap="delete"
|
||||
>
|
||||
|
@ -89,7 +96,6 @@
|
|||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<tr class="nav-item">
|
||||
<td>Open AppFlowy</td>
|
||||
<td>
|
||||
<div hx-post="/web-api/open_app" class="svg-container button">
|
||||
<div hx-post="../web-api/open_app" class="svg-container button">
|
||||
{% include "../assets/logo.html" %}
|
||||
</div>
|
||||
</td>
|
||||
|
@ -10,7 +10,10 @@
|
|||
<tr class="nav-item">
|
||||
<td>Download AppFlowy</td>
|
||||
<td>
|
||||
<div onclick="window.location.href='https://appflowy.io/download';" class="svg-container button">
|
||||
<div
|
||||
onclick="window.location.href='https://appflowy.io/download';"
|
||||
class="svg-container button"
|
||||
>
|
||||
{% include "../assets/logo.html" %}
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<table class="cyan-table table">
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Owner Name</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for shared_workspace in shared_workspaces %}
|
||||
<tr>
|
||||
<th>Workspace Name</th>
|
||||
<th>Owner Name</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for shared_workspace in shared_workspaces %}
|
||||
<tr>
|
||||
<td> {{ shared_workspace.workspace_name|escape }} </td>
|
||||
<td> {{ shared_workspace.owner_name|escape }} </td>
|
||||
<td>{{ shared_workspace.workspace_name|escape }}</td>
|
||||
<td>{{ shared_workspace.owner_name|escape }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="button red"
|
||||
hx-post="/web-api/workspace/{{ shared_workspace.workspace_id|escape }}/leave"
|
||||
hx-post="../web-api/workspace/{{ shared_workspace.workspace_id|escape }}/leave"
|
||||
hx-confirm="Are you sure?"
|
||||
hx-target="closest tr"
|
||||
hx-swap="delete"
|
||||
|
@ -22,5 +22,5 @@
|
|||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/navigate"
|
||||
hx-get="../web/components/user/navigate"
|
||||
data-section="navigate"
|
||||
>
|
||||
Navigate
|
||||
|
@ -10,7 +10,7 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/change-password"
|
||||
hx-get="../web/components/user/change-password"
|
||||
data-section="change-password"
|
||||
>
|
||||
Change Password
|
||||
|
@ -18,7 +18,7 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/invite"
|
||||
hx-get="../web/components/user/invite"
|
||||
data-section="invite"
|
||||
>
|
||||
Invite
|
||||
|
@ -26,7 +26,7 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/user-usage"
|
||||
hx-get="../web/components/user/user-usage"
|
||||
data-section="user-usage"
|
||||
>
|
||||
User Usage
|
||||
|
@ -34,7 +34,7 @@
|
|||
<div
|
||||
class="sidebar-item"
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/workspace-usage"
|
||||
hx-get="../web/components/user/workspace-usage"
|
||||
data-section="workspace-usage"
|
||||
>
|
||||
Workspace Usage
|
||||
|
@ -42,14 +42,14 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const frag = window.location.href.split('#');
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
const frag = window.location.href.split("#");
|
||||
if (frag.length > 1) {
|
||||
const section = frag[1];
|
||||
const sidebarItems = document.querySelectorAll('.sidebar-item');
|
||||
const sidebarItems = document.querySelectorAll(".sidebar-item");
|
||||
|
||||
sidebarItems.forEach(item => {
|
||||
if (item.getAttribute('data-section') === section) {
|
||||
sidebarItems.forEach((item) => {
|
||||
if (item.getAttribute("data-section") === section) {
|
||||
item.click();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
<div
|
||||
hx-target="#sidebar-content"
|
||||
hx-get="/web/components/user/user"
|
||||
hx-get="../web/components/user/user"
|
||||
class="button cyan"
|
||||
>
|
||||
{{ user.email|escape }}
|
||||
</div>
|
||||
<div
|
||||
hx-delete="/web-api/delete-account"
|
||||
hx-delete="../web-api/delete-account"
|
||||
hx-confirm="This will erase all data associated with this account. Are you sure?"
|
||||
class="button red"
|
||||
>
|
||||
|
@ -27,12 +27,12 @@
|
|||
document
|
||||
.getElementById("adminBtn")
|
||||
.addEventListener("click", function () {
|
||||
window.location.href = "/web/admin/home";
|
||||
window.location.href = "../web/admin/home";
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="button yellow" id="logoutBtn" hx-post="/web-api/logout">
|
||||
<div class="button yellow" id="logoutBtn" hx-post="../web-api/logout">
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="/assets/base.css" rel="stylesheet" />
|
||||
<link href="/assets/message.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/base.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/message.css" rel="stylesheet" />
|
||||
<title>{% block title %}{{ title|escape }}{% endblock %}</title>
|
||||
<script
|
||||
src="https://unpkg.com/htmx.org@1.9.6"
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<!-- prettier-ignore -->
|
||||
{% block head %}
|
||||
<link href="/assets/sidebar.css" rel="stylesheet" />
|
||||
<link href="/assets/top_menu_bar.css" rel="stylesheet" />
|
||||
<link href="/assets/home.css" rel="stylesheet" />
|
||||
<link href="/assets/navigate.css" rel="stylesheet" />
|
||||
<link href="../../assets/sidebar.css" rel="stylesheet" />
|
||||
<link href="../../assets/top_menu_bar.css" rel="stylesheet" />
|
||||
<link href="../../assets/home.css" rel="stylesheet" />
|
||||
<link href="../../assets/navigate.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<!-- prettier-ignore -->
|
||||
{% block head %}
|
||||
<link href="/assets/sidebar.css" rel="stylesheet" />
|
||||
<link href="/assets/top_menu_bar.css" rel="stylesheet" />
|
||||
<link href="/assets/home.css" rel="stylesheet" />
|
||||
<link href="/assets/navigate.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/sidebar.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/top_menu_bar.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/home.css" rel="stylesheet" />
|
||||
<link href="{{ path_prefix }}/assets/navigate.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.history.replaceState(null, '', '/web/home');
|
||||
window.history.replaceState(null, "", "{{ path_prefix }}/web/home");
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
<!-- prettier-ignore -->
|
||||
{% block head %}
|
||||
<link href="/assets/login.css" rel="stylesheet" />
|
||||
<link href="/assets/google/logo.css" rel="stylesheet" />
|
||||
<link href="../assets/login.css" rel="stylesheet" />
|
||||
<link href="../assets/google/logo.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
@ -16,14 +16,14 @@
|
|||
<div id="login-signin">
|
||||
<div id="login-splash">
|
||||
{% include "../assets/logo.html" %}
|
||||
<h2 style="padding: 16px;">AppFlowy Cloud</h2>
|
||||
<h2 style="padding: 16px">AppFlowy Cloud</h2>
|
||||
</div>
|
||||
|
||||
<h3>Email Login</h3>
|
||||
<form>
|
||||
<table style="width: 100%">
|
||||
{% if let Some(redirect_to) = redirect_to %}
|
||||
<input type="hidden" name="redirect_to" value="{{ redirect_to }}">
|
||||
<input type="hidden" name="redirect_to" value="{{ redirect_to }}" />
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
|
@ -61,7 +61,7 @@
|
|||
>
|
||||
<div style="display: flex; margin: 8px 0px">
|
||||
<button
|
||||
hx-post="/web-api/signin"
|
||||
hx-post="../web-api/signin"
|
||||
hx-target="#none"
|
||||
class="button cyan"
|
||||
type="submit"
|
||||
|
@ -70,7 +70,7 @@
|
|||
Sign In
|
||||
</button>
|
||||
<button
|
||||
hx-post="/web-api/signup"
|
||||
hx-post="../web-api/signup"
|
||||
hx-target="#none"
|
||||
class="button purple"
|
||||
type="submit"
|
||||
|
@ -84,13 +84,13 @@
|
|||
<!-- Load OAuth Providers if configured -->
|
||||
{% if oauth_providers.len() > 0 %}
|
||||
<br />
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr style="display: flex; align-items: center;">
|
||||
<td style="width: 100%; margin: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse">
|
||||
<tr style="display: flex; align-items: center">
|
||||
<td style="width: 100%; margin: auto">
|
||||
<hr class="divider" />
|
||||
</td>
|
||||
<td style="flex: 1; text-align: center;"> or </td>
|
||||
<td style="width: 100%; margin: auto;">
|
||||
<td style="flex: 1; text-align: center"> or </td>
|
||||
<td style="width: 100%; margin: auto">
|
||||
<hr class="divider" />
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -98,14 +98,30 @@
|
|||
|
||||
<h3>OAuth Login</h3>
|
||||
<div id="oauth-container">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: center">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
{% for provider in oauth_providers %}
|
||||
<a
|
||||
href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to={{ oauth_redirect_to|default("/web/login-callback")|escape }}"
|
||||
href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to={{ oauth_redirect_to|escape }}"
|
||||
style="text-decoration: none; color: inherit"
|
||||
>
|
||||
<div style="display: flex; align-items: center; border: 1px solid #384967; margin: 4px; border-radius: 4px; height: 64px">
|
||||
<div>   {{ provider }} </div>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #384967;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
height: 64px;
|
||||
"
|
||||
>
|
||||
<div>  {{ provider }}</div>
|
||||
<div class="oauth-icon">
|
||||
<div
|
||||
hx-get="../assets/{{ provider|escape }}/logo.html"
|
||||
|
@ -122,7 +138,12 @@
|
|||
|
||||
<span>   </span>
|
||||
<div style="max-width: 256px; display: flex; align-items: center">
|
||||
<img src="https://cdn.prod.website-files.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" alt="kofi" width="32" height="32">
|
||||
<img
|
||||
src="https://cdn.prod.website-files.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png"
|
||||
alt="kofi"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
<i>
|
||||
  Support AppFlowy on <a href="https://ko-fi.com/appflowy">Ko-fi</a>
|
||||
</i>
|
||||
|
@ -130,12 +151,14 @@
|
|||
|
||||
<span>   </span>
|
||||
<div style="max-width: 256px">
|
||||
<small style="color: #888; text-align: center;"><i>
|
||||
 
|
||||
By clicking logging in or signing up, you confirm that you have read, understood, and agreed to AppFlowy's
|
||||
<a href="https://appflowy.io/terms">Terms</a> and
|
||||
<a href="https://appflowy.io/privacy">Privacy Policy</a>.
|
||||
</i></small>
|
||||
<small style="color: #888; text-align: center"
|
||||
><i>
|
||||
  By clicking logging in or signing up, you confirm that you have
|
||||
read, understood, and agreed to AppFlowy's
|
||||
<a href="https://appflowy.io/terms">Terms</a> and
|
||||
<a href="https://appflowy.io/privacy">Privacy Policy</a>.
|
||||
</i></small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<script>
|
||||
window.onload = function() {
|
||||
window.onload = function () {
|
||||
const current_url = window.location.href;
|
||||
if (current_url.includes('/login-callback')) {
|
||||
var redirect_url = current_url.replace('/login-callback', '/login-callback-query');
|
||||
if (redirect_url.includes('?')) { // If '?' exists, replace '#' with '&'
|
||||
redirect_url = redirect_url.replace('#', '&');
|
||||
} else { // If '?' does not exist, replace '#' with '?'
|
||||
redirect_url = redirect_url.replace('#', '?');
|
||||
if (current_url.includes("/login-callback")) {
|
||||
var redirect_url = current_url.replace(
|
||||
"/login-callback",
|
||||
"/login-callback-query",
|
||||
);
|
||||
if (redirect_url.includes("?")) {
|
||||
// If '?' exists, replace '#' with '&'
|
||||
redirect_url = redirect_url.replace("#", "&");
|
||||
} else {
|
||||
// If '?' does not exist, replace '#' with '?'
|
||||
redirect_url = redirect_url.replace("#", "?");
|
||||
}
|
||||
window.location.href = redirect_url;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
<!-- prettier-ignore -->
|
||||
{% block head %}
|
||||
<link href="/assets/login.css" rel="stylesheet" />
|
||||
<link href="/assets/google/logo.css" rel="stylesheet" />
|
||||
<link href="../assets/login.css" rel="stylesheet" />
|
||||
<link href="../assets/google/logo.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
@ -33,7 +33,7 @@
|
|||
placeholder="Please enter your email address"
|
||||
/>
|
||||
<button
|
||||
hx-post="/web-api/signin"
|
||||
hx-post="../web-api/signin"
|
||||
hx-target="#none"
|
||||
class="button cyan"
|
||||
type="submit"
|
||||
|
@ -63,7 +63,7 @@
|
|||
{% for provider in oauth_providers %}
|
||||
<div class="oauth-item-inner">
|
||||
<a
|
||||
href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to={{ oauth_redirect_to|default("/web/login-callback")|escape }}"
|
||||
href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to={{ oauth_redirect_to|escape }}"
|
||||
style="text-decoration: none; color: inherit"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: center; color: inherit">
|
||||
|
@ -122,4 +122,3 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -30,7 +30,10 @@ impl AdminFrontendClient {
|
|||
}
|
||||
|
||||
pub async fn web_api_sign_in(&mut self, email: &str, password: &str) {
|
||||
let url = format!("{}/web-api/signin", self.test_config.hostname);
|
||||
let url = format!(
|
||||
"{}{}/web-api/signin",
|
||||
self.test_config.hostname, self.server_config.path_prefix
|
||||
);
|
||||
let resp = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
|
@ -51,7 +54,10 @@ impl AdminFrontendClient {
|
|||
&mut self,
|
||||
oauth_redirect: &OAuthRedirect,
|
||||
) -> reqwest::Response {
|
||||
let url = format!("{}/web-api/oauth-redirect", self.test_config.hostname);
|
||||
let url = format!(
|
||||
"{}{}/web-api/oauth-redirect",
|
||||
self.test_config.hostname, self.server_config.path_prefix
|
||||
);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
|
@ -70,7 +76,10 @@ impl AdminFrontendClient {
|
|||
&mut self,
|
||||
oauth_redirect: &OAuthRedirectToken,
|
||||
) -> reqwest::Response {
|
||||
let url = format!("{}/web-api/oauth-redirect/token", self.test_config.hostname);
|
||||
let url = format!(
|
||||
"{}{}/web-api/oauth-redirect/token",
|
||||
self.test_config.hostname, self.server_config.path_prefix
|
||||
);
|
||||
self
|
||||
.http_client
|
||||
.get(&url)
|
||||
|
|
|
@ -45,6 +45,10 @@ ADMIN_FRONTEND_REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
|||
ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999
|
||||
## URL that connects to the cloud docker container
|
||||
ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=http://appflowy_cloud:8000
|
||||
## Base Url for the admin frontend. If you use the default Nginx conf provided here, this value should be /console.
|
||||
## If you want to keep the previous behaviour where admin frontend is served at the root, don't set this env variable,
|
||||
## or set it to empty string.
|
||||
ADMIN_FRONTEND_PATH_PREFIX=/console
|
||||
|
||||
# authentication key, change this and keep the key safe and secret
|
||||
# self defined key, you can use any string
|
||||
|
|
|
@ -212,4 +212,5 @@ performed via the admin portal as opposed to links provided in emails.
|
|||
- Update the docker compose file such that the ports for `appflowy_cloud`, `gotrue`, and `admin_frontend` are mapped
|
||||
to different ports on the host server. If possible, use firewall to make sure that these ports are not accessible
|
||||
from the internet.
|
||||
- Update `proxy_pass` in `nginx/nginx.conf` to point to the above ports.
|
||||
- Update `proxy_pass` in `nginx/nginx.conf` to point to the above ports. Then adapt this configuration for your
|
||||
existing Nginx configuration.
|
||||
|
|
|
@ -30,7 +30,7 @@ After executing `docker-compose up -d`, AppFlowy-Cloud is accessible at `http://
|
|||
- `/gotrue`: Redirects to the GoTrue Auth Server.
|
||||
- `/api`: AppFlowy-Cloud's HTTP API endpoint.
|
||||
- `/ws`: WebSocket endpoint for AppFlowy-Cloud.
|
||||
- `/web`: User Admin Frontend for AppFlowy.
|
||||
- `/console`: User Admin Frontend for AppFlowy.
|
||||
- `/pgadmin`: Interface for Postgres database management.
|
||||
- `/minio`: User interface for Minio object storage.
|
||||
- `/portainer`: Tool for container management.
|
||||
|
|
|
@ -141,6 +141,7 @@ services:
|
|||
- APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL}
|
||||
- APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD}
|
||||
- AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
|
||||
- APPFLOWY_ADMIN_FRONTEND_PATH_PREFIX=${ADMIN_FRONTEND_PATH_PREFIX}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
@ -165,6 +166,7 @@ services:
|
|||
- ADMIN_FRONTEND_REDIS_URL=${ADMIN_FRONTEND_REDIS_URL:-redis://redis:6379}
|
||||
- ADMIN_FRONTEND_GOTRUE_URL=${ADMIN_FRONTEND_GOTRUE_URL:-http://gotrue:9999}
|
||||
- ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=${ADMIN_FRONTEND_APPFLOWY_CLOUD_URL:-http://appflowy_cloud:8000}
|
||||
- ADMIN_FRONTEND_PATH_PREFIX=${ADMIN_FRONTEND_PATH_PREFIX:-}
|
||||
depends_on:
|
||||
appflowy_cloud:
|
||||
condition: service_started
|
||||
|
|
|
@ -136,6 +136,7 @@ services:
|
|||
- AI_SERVER_HOST=${AI_SERVER_HOST}
|
||||
- AI_SERVER_PORT=${AI_SERVER_PORT}
|
||||
- AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
|
||||
- APPFLOWY_ADMIN_FRONTEND_PATH_PREFIX=${ADMIN_FRONTEND_PATH_PREFIX}
|
||||
# Uncomment this line if AppFlowy Web has been deployed
|
||||
# - APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL}
|
||||
build:
|
||||
|
@ -159,6 +160,7 @@ services:
|
|||
- ADMIN_FRONTEND_REDIS_URL=${ADMIN_FRONTEND_REDIS_URL:-redis://redis:6379}
|
||||
- ADMIN_FRONTEND_GOTRUE_URL=${ADMIN_FRONTEND_GOTRUE_URL:-http://gotrue:9999}
|
||||
- ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=${ADMIN_FRONTEND_APPFLOWY_CLOUD_URL:-http://appflowy_cloud:8000}
|
||||
- ADMIN_FRONTEND_PATH_PREFIX=${ADMIN_FRONTEND_PATH_PREFIX:-}
|
||||
depends_on:
|
||||
appflowy_cloud:
|
||||
condition: service_started
|
||||
|
|
|
@ -218,7 +218,7 @@ http {
|
|||
|
||||
# Admin Frontend
|
||||
# Optional Module, comment this section if you are did not deploy admin_frontend in docker-compose.yml
|
||||
location / {
|
||||
location /console {
|
||||
proxy_pass $admin_frontend_backend;
|
||||
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
|
|
|
@ -453,6 +453,7 @@ async fn post_workspace_invite_handler(
|
|||
&workspace_id,
|
||||
invitations,
|
||||
state.config.appflowy_web_url.as_deref(),
|
||||
&state.config.admin_frontend_path_prefix,
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
|
|
|
@ -361,6 +361,7 @@ pub async fn invite_workspace_members(
|
|||
workspace_id: &Uuid,
|
||||
invitations: Vec<WorkspaceMemberInvitation>,
|
||||
appflowy_web_url: Option<&str>,
|
||||
admin_frontend_path_prefix: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let mut txn = pg_pool
|
||||
.begin()
|
||||
|
@ -432,7 +433,10 @@ pub async fn invite_workspace_members(
|
|||
// Generate a link such that when clicked, the user is added to the workspace.
|
||||
let accept_url = {
|
||||
match appflowy_web_url {
|
||||
Some(appflowy_web_url) => format!("{}/accept-invitation?invited_id={}", appflowy_web_url, invite_id),
|
||||
Some(appflowy_web_url) => format!(
|
||||
"{}/accept-invitation?invited_id={}",
|
||||
appflowy_web_url, invite_id
|
||||
),
|
||||
None => {
|
||||
gotrue_client
|
||||
.admin_generate_link(
|
||||
|
@ -441,8 +445,10 @@ pub async fn invite_workspace_members(
|
|||
type_: GenerateLinkType::MagicLink,
|
||||
email: invitation.email.clone(),
|
||||
redirect_to: format!(
|
||||
"/web/login-callback?action=accept_workspace_invite&workspace_invitation_id={}&workspace_name={}&workspace_icon={}&user_name={}&user_icon={}&workspace_member_count={}",
|
||||
invite_id, workspace_name,
|
||||
"{}/web/login-callback?action=accept_workspace_invite&workspace_invitation_id={}&workspace_name={}&workspace_icon={}&user_name={}&user_icon={}&workspace_member_count={}",
|
||||
admin_frontend_path_prefix,
|
||||
invite_id,
|
||||
workspace_name,
|
||||
workspace_icon_url,
|
||||
inviter_name,
|
||||
user_icon_url,
|
||||
|
|
|
@ -27,6 +27,7 @@ pub struct Config {
|
|||
pub mailer: MailerSetting,
|
||||
pub apple_oauth: AppleOAuthSetting,
|
||||
pub appflowy_web_url: Option<String>,
|
||||
pub admin_frontend_path_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
|
@ -262,6 +263,7 @@ pub fn get_configuration() -> Result<Config, anyhow::Error> {
|
|||
client_secret: get_env_var("APPFLOWY_APPLE_OAUTH_CLIENT_SECRET", "").into(),
|
||||
},
|
||||
appflowy_web_url: get_env_var_opt("APPFLOWY_WEB_URL"),
|
||||
admin_frontend_path_prefix: get_env_var("APPFLOWY_ADMIN_FRONTEND_PATH_PREFIX", ""),
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue