app: make it possible to serve lldap behind a sub-path

This commit is contained in:
MinerSebas 2023-12-05 10:24:25 +01:00 committed by nitnelave
parent ec0737c58a
commit 70d85524db
6 changed files with 88 additions and 35 deletions

View file

@ -4,7 +4,8 @@
<head>
<meta charset="utf-8" />
<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
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
rel="preload stylesheet"
@ -33,7 +34,7 @@
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
<link
rel="stylesheet"
href="/static/style.css" />
href="static/style.css" />
<script>
function inDarkMode(){
return darkmode.inDarkMode;

View file

@ -268,7 +268,7 @@ impl App {
<header class="p-2 mb-3 border-bottom">
<div class="container">
<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>
</a>

View file

@ -18,6 +18,10 @@ fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
const NO_BODY: Option<()> = None;
fn base_url() -> String {
yew_router::utils::base_url().unwrap_or_default()
}
async fn call_server(
url: &str,
body: Option<impl Serialize>,
@ -97,7 +101,7 @@ impl HostService {
};
let request_body = QueryType::build_query(variables);
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
"/api/graphql",
&(base_url() + "/api/graphql"),
Some(request_body),
error_message,
)
@ -109,7 +113,7 @@ impl HostService {
request: login::ClientLoginStartRequest,
) -> Result<Box<login::ServerLoginStartResponse>> {
call_server_json_with_error_message(
"/auth/opaque/login/start",
&(base_url() + "/auth/opaque/login/start"),
Some(request),
"Could not start authentication: ",
)
@ -118,7 +122,7 @@ impl HostService {
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
"/auth/opaque/login/finish",
&(base_url() + "/auth/opaque/login/finish"),
Some(request),
"Could not finish authentication",
)
@ -130,7 +134,7 @@ impl HostService {
request: registration::ClientRegistrationStartRequest,
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
call_server_json_with_error_message(
"/auth/opaque/register/start",
&(base_url() + "/auth/opaque/register/start"),
Some(request),
"Could not start registration: ",
)
@ -141,7 +145,7 @@ impl HostService {
request: registration::ClientRegistrationFinishRequest,
) -> Result<()> {
call_server_empty_response_with_error_message(
"/auth/opaque/register/finish",
&(base_url() + "/auth/opaque/register/finish"),
Some(request),
"Could not finish registration",
)
@ -150,7 +154,7 @@ impl HostService {
pub async fn refresh() -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
"/auth/refresh",
&(base_url() + "/auth/refresh"),
NO_BODY,
"Could not start authentication: ",
)
@ -160,13 +164,21 @@ impl HostService {
// The `_request` parameter is to make it the same shape as the other functions.
pub async fn logout() -> Result<()> {
call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout")
.await
call_server_empty_response_with_error_message(
&(base_url() + "/auth/logout"),
NO_BODY,
"Could not logout",
)
.await
}
pub async fn reset_password_step1(username: String) -> Result<()> {
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,
"Could not initiate password reset",
)
@ -177,7 +189,7 @@ impl HostService {
token: String,
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
call_server_json_with_error_message(
&format!("/auth/reset/step2/{}", token),
&format!("{}/auth/reset/step2/{}", base_url(), token),
NO_BODY,
"Could not validate token",
)
@ -185,13 +197,13 @@ impl HostService {
}
pub async fn probe_password_reset() -> Result<bool> {
Ok(
gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND,
Ok(gloo_net::http::Request::get(
&(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
)
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND)
}
}

View file

@ -22,10 +22,11 @@ pub fn set_cookie(cookie_name: &str, value: &str, expiration: &DateTime<Utc>) ->
.map_err(|_| anyhow!("Document is not an HTMLDocument"))
})?;
let cookie_string = format!(
"{}={}; expires={}; sameSite=Strict; path=/",
"{}={}; expires={}; sameSite=Strict; path={}/",
cookie_name,
value,
expiration.to_rfc2822()
expiration.to_rfc2822(),
yew_router::utils::base_url().unwrap_or_default()
);
doc.set_cookie(&cookie_string)
.map_err(|_| anyhow!("Could not set cookie"))

View file

@ -112,13 +112,17 @@ where
"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 token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await;
Ok(HttpResponse::Ok()
.cookie(
Cookie::build("token", token.as_str())
.max_age(1.days())
.path("/")
.path(&path)
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@ -239,12 +243,16 @@ where
.await;
let groups = HashSet::new();
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()
.cookie(
Cookie::build("token", token.as_str())
.max_age(5.minutes())
// Cookie is only valid to reset the password.
.path("/auth")
.path(format!("{}auth", path))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@ -284,11 +292,15 @@ where
for jwt_hash in new_blacklisted_jwt_hashes {
jwt_blacklist.insert(jwt_hash);
}
let mut path = data.server_url.path().to_string();
if !path.ends_with('/') {
path.push('/');
};
Ok(HttpResponse::Ok()
.cookie(
Cookie::build("token", "")
.max_age(0.days())
.path("/")
.path(&path)
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@ -296,7 +308,7 @@ where
.cookie(
Cookie::build("refresh_token", "")
.max_age(0.days())
.path("/auth")
.path(format!("{}auth", path))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@ -351,12 +363,15 @@ where
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 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()
.cookie(
Cookie::build("token", token.as_str())
.max_age(1.days())
.path("/")
.path(&path)
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@ -364,7 +379,7 @@ where
.cookie(
Cookie::build("refresh_token", refresh_token_plus_name.clone())
.max_age(max_age.num_days().days())
.path("/auth")
.path(format!("{}auth", path))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),

View file

@ -12,7 +12,7 @@ use crate::{
tcp_backend_handler::*,
},
};
use actix_files::{Files, NamedFile};
use actix_files::Files;
use actix_http::{header, HttpServiceBuilder};
use actix_server::ServerBuilder;
use actix_service::map_config;
@ -21,13 +21,22 @@ use anyhow::{Context, Result};
use hmac::Hmac;
use sha2::Sha512;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::RwLock;
use tracing::info;
async fn index() -> actix_web::Result<NamedFile> {
let path = PathBuf::from(r"app/index.html");
Ok(NamedFile::open(path)?)
async fn index<Backend>(data: web::Data<AppState<Backend>>) -> actix_web::Result<impl Responder> {
let mut file = std::fs::read_to_string(r"./app/index.html")?;
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)]
@ -68,6 +77,20 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
.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> {
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)),
)
.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.
.service(Files::new("/pkg", "./app/pkg"))
// Serve static files
@ -125,7 +149,7 @@ fn http_config<Backend>(
// Serve static fonts
.service(Files::new("/static/fonts", "./app/static/fonts"))
// 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> {