mirror of
https://github.com/lldap/lldap.git
synced 2026-03-31 15:07:48 +01:00
app: make it possible to serve lldap behind a sub-path
This commit is contained in:
+3
-2
@@ -4,7 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>LLDAP Administration</title>
|
<title>LLDAP Administration</title>
|
||||||
<script src="/static/main.js" type="module" defer></script>
|
<base href="/">
|
||||||
|
<script src="static/main.js" type="module" defer></script>
|
||||||
<link
|
<link
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
|
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
|
||||||
rel="preload stylesheet"
|
rel="preload stylesheet"
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
|
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="/static/style.css" />
|
href="static/style.css" />
|
||||||
<script>
|
<script>
|
||||||
function inDarkMode(){
|
function inDarkMode(){
|
||||||
return darkmode.inDarkMode;
|
return darkmode.inDarkMode;
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ impl App {
|
|||||||
<header class="p-2 mb-3 border-bottom">
|
<header class="p-2 mb-3 border-bottom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
|
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
|
||||||
<a href="/" class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
|
<a href={yew_router::utils::base_url().unwrap_or("/".to_string())} class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
|
||||||
<h2>{"LLDAP"}</h2>
|
<h2>{"LLDAP"}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
+29
-17
@@ -18,6 +18,10 @@ fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
|
|||||||
|
|
||||||
const NO_BODY: Option<()> = None;
|
const NO_BODY: Option<()> = None;
|
||||||
|
|
||||||
|
fn base_url() -> String {
|
||||||
|
yew_router::utils::base_url().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
async fn call_server(
|
async fn call_server(
|
||||||
url: &str,
|
url: &str,
|
||||||
body: Option<impl Serialize>,
|
body: Option<impl Serialize>,
|
||||||
@@ -97,7 +101,7 @@ impl HostService {
|
|||||||
};
|
};
|
||||||
let request_body = QueryType::build_query(variables);
|
let request_body = QueryType::build_query(variables);
|
||||||
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
|
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
|
||||||
"/api/graphql",
|
&(base_url() + "/api/graphql"),
|
||||||
Some(request_body),
|
Some(request_body),
|
||||||
error_message,
|
error_message,
|
||||||
)
|
)
|
||||||
@@ -109,7 +113,7 @@ impl HostService {
|
|||||||
request: login::ClientLoginStartRequest,
|
request: login::ClientLoginStartRequest,
|
||||||
) -> Result<Box<login::ServerLoginStartResponse>> {
|
) -> Result<Box<login::ServerLoginStartResponse>> {
|
||||||
call_server_json_with_error_message(
|
call_server_json_with_error_message(
|
||||||
"/auth/opaque/login/start",
|
&(base_url() + "/auth/opaque/login/start"),
|
||||||
Some(request),
|
Some(request),
|
||||||
"Could not start authentication: ",
|
"Could not start authentication: ",
|
||||||
)
|
)
|
||||||
@@ -118,7 +122,7 @@ impl HostService {
|
|||||||
|
|
||||||
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
|
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
|
||||||
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
||||||
"/auth/opaque/login/finish",
|
&(base_url() + "/auth/opaque/login/finish"),
|
||||||
Some(request),
|
Some(request),
|
||||||
"Could not finish authentication",
|
"Could not finish authentication",
|
||||||
)
|
)
|
||||||
@@ -130,7 +134,7 @@ impl HostService {
|
|||||||
request: registration::ClientRegistrationStartRequest,
|
request: registration::ClientRegistrationStartRequest,
|
||||||
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
|
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
|
||||||
call_server_json_with_error_message(
|
call_server_json_with_error_message(
|
||||||
"/auth/opaque/register/start",
|
&(base_url() + "/auth/opaque/register/start"),
|
||||||
Some(request),
|
Some(request),
|
||||||
"Could not start registration: ",
|
"Could not start registration: ",
|
||||||
)
|
)
|
||||||
@@ -141,7 +145,7 @@ impl HostService {
|
|||||||
request: registration::ClientRegistrationFinishRequest,
|
request: registration::ClientRegistrationFinishRequest,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
call_server_empty_response_with_error_message(
|
call_server_empty_response_with_error_message(
|
||||||
"/auth/opaque/register/finish",
|
&(base_url() + "/auth/opaque/register/finish"),
|
||||||
Some(request),
|
Some(request),
|
||||||
"Could not finish registration",
|
"Could not finish registration",
|
||||||
)
|
)
|
||||||
@@ -150,7 +154,7 @@ impl HostService {
|
|||||||
|
|
||||||
pub async fn refresh() -> Result<(String, bool)> {
|
pub async fn refresh() -> Result<(String, bool)> {
|
||||||
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
||||||
"/auth/refresh",
|
&(base_url() + "/auth/refresh"),
|
||||||
NO_BODY,
|
NO_BODY,
|
||||||
"Could not start authentication: ",
|
"Could not start authentication: ",
|
||||||
)
|
)
|
||||||
@@ -160,13 +164,21 @@ impl HostService {
|
|||||||
|
|
||||||
// The `_request` parameter is to make it the same shape as the other functions.
|
// The `_request` parameter is to make it the same shape as the other functions.
|
||||||
pub async fn logout() -> Result<()> {
|
pub async fn logout() -> Result<()> {
|
||||||
call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout")
|
call_server_empty_response_with_error_message(
|
||||||
.await
|
&(base_url() + "/auth/logout"),
|
||||||
|
NO_BODY,
|
||||||
|
"Could not logout",
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reset_password_step1(username: String) -> Result<()> {
|
pub async fn reset_password_step1(username: String) -> Result<()> {
|
||||||
call_server_empty_response_with_error_message(
|
call_server_empty_response_with_error_message(
|
||||||
&format!("/auth/reset/step1/{}", url_escape::encode_query(&username)),
|
&format!(
|
||||||
|
"{}/auth/reset/step1/{}",
|
||||||
|
base_url(),
|
||||||
|
url_escape::encode_query(&username)
|
||||||
|
),
|
||||||
NO_BODY,
|
NO_BODY,
|
||||||
"Could not initiate password reset",
|
"Could not initiate password reset",
|
||||||
)
|
)
|
||||||
@@ -177,7 +189,7 @@ impl HostService {
|
|||||||
token: String,
|
token: String,
|
||||||
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
|
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
|
||||||
call_server_json_with_error_message(
|
call_server_json_with_error_message(
|
||||||
&format!("/auth/reset/step2/{}", token),
|
&format!("{}/auth/reset/step2/{}", base_url(), token),
|
||||||
NO_BODY,
|
NO_BODY,
|
||||||
"Could not validate token",
|
"Could not validate token",
|
||||||
)
|
)
|
||||||
@@ -185,13 +197,13 @@ impl HostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn probe_password_reset() -> Result<bool> {
|
pub async fn probe_password_reset() -> Result<bool> {
|
||||||
Ok(
|
Ok(gloo_net::http::Request::get(
|
||||||
gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
|
&(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.status()
|
|
||||||
!= http::StatusCode::NOT_FOUND,
|
|
||||||
)
|
)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status()
|
||||||
|
!= http::StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ pub fn set_cookie(cookie_name: &str, value: &str, expiration: &DateTime<Utc>) ->
|
|||||||
.map_err(|_| anyhow!("Document is not an HTMLDocument"))
|
.map_err(|_| anyhow!("Document is not an HTMLDocument"))
|
||||||
})?;
|
})?;
|
||||||
let cookie_string = format!(
|
let cookie_string = format!(
|
||||||
"{}={}; expires={}; sameSite=Strict; path=/",
|
"{}={}; expires={}; sameSite=Strict; path={}/",
|
||||||
cookie_name,
|
cookie_name,
|
||||||
value,
|
value,
|
||||||
expiration.to_rfc2822()
|
expiration.to_rfc2822(),
|
||||||
|
yew_router::utils::base_url().unwrap_or_default()
|
||||||
);
|
);
|
||||||
doc.set_cookie(&cookie_string)
|
doc.set_cookie(&cookie_string)
|
||||||
.map_err(|_| anyhow!("Could not set cookie"))
|
.map_err(|_| anyhow!("Could not set cookie"))
|
||||||
|
|||||||
@@ -112,13 +112,17 @@ where
|
|||||||
"Invalid refresh token".to_string(),
|
"Invalid refresh token".to_string(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
let mut path = data.server_url.path().to_string();
|
||||||
|
if !path.ends_with('/') {
|
||||||
|
path.push('/');
|
||||||
|
};
|
||||||
let groups = data.get_readonly_handler().get_user_groups(&user).await?;
|
let groups = data.get_readonly_handler().get_user_groups(&user).await?;
|
||||||
let token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await;
|
let token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await;
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("token", token.as_str())
|
Cookie::build("token", token.as_str())
|
||||||
.max_age(1.days())
|
.max_age(1.days())
|
||||||
.path("/")
|
.path(&path)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
@@ -239,12 +243,16 @@ where
|
|||||||
.await;
|
.await;
|
||||||
let groups = HashSet::new();
|
let groups = HashSet::new();
|
||||||
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, &user_id, groups).await;
|
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, &user_id, groups).await;
|
||||||
|
let mut path = data.server_url.path().to_string();
|
||||||
|
if !path.ends_with('/') {
|
||||||
|
path.push('/');
|
||||||
|
};
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("token", token.as_str())
|
Cookie::build("token", token.as_str())
|
||||||
.max_age(5.minutes())
|
.max_age(5.minutes())
|
||||||
// Cookie is only valid to reset the password.
|
// Cookie is only valid to reset the password.
|
||||||
.path("/auth")
|
.path(format!("{}auth", path))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
@@ -284,11 +292,15 @@ where
|
|||||||
for jwt_hash in new_blacklisted_jwt_hashes {
|
for jwt_hash in new_blacklisted_jwt_hashes {
|
||||||
jwt_blacklist.insert(jwt_hash);
|
jwt_blacklist.insert(jwt_hash);
|
||||||
}
|
}
|
||||||
|
let mut path = data.server_url.path().to_string();
|
||||||
|
if !path.ends_with('/') {
|
||||||
|
path.push('/');
|
||||||
|
};
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("token", "")
|
Cookie::build("token", "")
|
||||||
.max_age(0.days())
|
.max_age(0.days())
|
||||||
.path("/")
|
.path(&path)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
@@ -296,7 +308,7 @@ where
|
|||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("refresh_token", "")
|
Cookie::build("refresh_token", "")
|
||||||
.max_age(0.days())
|
.max_age(0.days())
|
||||||
.path("/auth")
|
.path(format!("{}auth", path))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
@@ -351,12 +363,15 @@ where
|
|||||||
let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?;
|
let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?;
|
||||||
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, name, groups).await;
|
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, name, groups).await;
|
||||||
let refresh_token_plus_name = refresh_token + "+" + name.as_str();
|
let refresh_token_plus_name = refresh_token + "+" + name.as_str();
|
||||||
|
let mut path = data.server_url.path().to_string();
|
||||||
|
if !path.ends_with('/') {
|
||||||
|
path.push('/');
|
||||||
|
};
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("token", token.as_str())
|
Cookie::build("token", token.as_str())
|
||||||
.max_age(1.days())
|
.max_age(1.days())
|
||||||
.path("/")
|
.path(&path)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
@@ -364,7 +379,7 @@ where
|
|||||||
.cookie(
|
.cookie(
|
||||||
Cookie::build("refresh_token", refresh_token_plus_name.clone())
|
Cookie::build("refresh_token", refresh_token_plus_name.clone())
|
||||||
.max_age(max_age.num_days().days())
|
.max_age(max_age.num_days().days())
|
||||||
.path("/auth")
|
.path(format!("{}auth", path))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
tcp_backend_handler::*,
|
tcp_backend_handler::*,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use actix_files::{Files, NamedFile};
|
use actix_files::Files;
|
||||||
use actix_http::{header, HttpServiceBuilder};
|
use actix_http::{header, HttpServiceBuilder};
|
||||||
use actix_server::ServerBuilder;
|
use actix_server::ServerBuilder;
|
||||||
use actix_service::map_config;
|
use actix_service::map_config;
|
||||||
@@ -21,13 +21,22 @@ use anyhow::{Context, Result};
|
|||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use sha2::Sha512;
|
use sha2::Sha512;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
async fn index() -> actix_web::Result<NamedFile> {
|
async fn index<Backend>(data: web::Data<AppState<Backend>>) -> actix_web::Result<impl Responder> {
|
||||||
let path = PathBuf::from(r"app/index.html");
|
let mut file = std::fs::read_to_string(r"./app/index.html")?;
|
||||||
Ok(NamedFile::open(path)?)
|
|
||||||
|
if data.server_url.path() != "/" {
|
||||||
|
file = file.replace(
|
||||||
|
"<base href=\"/\">",
|
||||||
|
format!("<base href=\"{}/\">", data.server_url.path()).as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file
|
||||||
|
.customize()
|
||||||
|
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8")))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@@ -68,6 +77,20 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
|
|||||||
.body(error.to_string())
|
.body(error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn main_js_handler<Backend>(
|
||||||
|
data: web::Data<AppState<Backend>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let mut file = std::fs::read_to_string(r"./app/static/main.js")?;
|
||||||
|
|
||||||
|
if data.server_url.path() != "/" {
|
||||||
|
file = file.replace("/pkg/", format!("{}/pkg/", data.server_url.path()).as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file
|
||||||
|
.customize()
|
||||||
|
.insert_header((header::CONTENT_TYPE, "text/javascript")))
|
||||||
|
}
|
||||||
|
|
||||||
async fn wasm_handler() -> actix_web::Result<impl Responder> {
|
async fn wasm_handler() -> actix_web::Result<impl Responder> {
|
||||||
Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?)
|
Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?)
|
||||||
}
|
}
|
||||||
@@ -118,6 +141,7 @@ fn http_config<Backend>(
|
|||||||
web::resource("/pkg/lldap_app_bg.wasm.gz").route(web::route().to(wasm_handler_compressed)),
|
web::resource("/pkg/lldap_app_bg.wasm.gz").route(web::route().to(wasm_handler_compressed)),
|
||||||
)
|
)
|
||||||
.service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler)))
|
.service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler)))
|
||||||
|
.service(web::resource("/static/main.js").route(web::route().to(main_js_handler::<Backend>)))
|
||||||
// Serve the /pkg path with the compiled WASM app.
|
// Serve the /pkg path with the compiled WASM app.
|
||||||
.service(Files::new("/pkg", "./app/pkg"))
|
.service(Files::new("/pkg", "./app/pkg"))
|
||||||
// Serve static files
|
// Serve static files
|
||||||
@@ -125,7 +149,7 @@ fn http_config<Backend>(
|
|||||||
// Serve static fonts
|
// Serve static fonts
|
||||||
.service(Files::new("/static/fonts", "./app/static/fonts"))
|
.service(Files::new("/static/fonts", "./app/static/fonts"))
|
||||||
// Default to serve index.html for unknown routes, to support routing.
|
// Default to serve index.html for unknown routes, to support routing.
|
||||||
.default_service(web::route().guard(guard::Get()).to(index));
|
.default_service(web::route().guard(guard::Get()).to(index::<Backend>));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct AppState<Backend> {
|
pub(crate) struct AppState<Backend> {
|
||||||
|
|||||||
Reference in New Issue
Block a user