use crate::web_client::authentication::{IsReadOnly, SessionTokenHash}; use crate::web_client::types::{AppState, CreateClientIdResponse, LoginRequest, LoginResponse}; use crate::web_client::utils::{get_mime_type, parse_cookies}; use axum::{ extract::{Path as AxumPath, Request, State}, http::{header, StatusCode}, response::{Html, IntoResponse}, Json, }; use axum_extra::extract::cookie::{Cookie, SameSite}; use include_dir; use uuid::Uuid; use zellij_utils::{consts::VERSION, web_authentication_tokens::create_session_token}; fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } const WEB_CLIENT_PAGE: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/", "assets/index.html" )); const ASSETS_DIR: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/assets"); pub async fn serve_html(State(state): State, request: Request) -> Html { let cookies = parse_cookies(&request); let is_authenticated = cookies.get("session_token").is_some(); let auth_value = if is_authenticated { "true" } else { "false" }; let base_url = html_escape( &state .config .lock() .unwrap() .web_client .base_url .clone() .unwrap_or("/".to_string()), ); let html = Html( WEB_CLIENT_PAGE .replace("IS_AUTHENTICATED", &format!("{}", auth_value)) .replace("BASE_URL", &base_url), ); html } pub async fn login_handler( State(state): State, Json(login_request): Json, ) -> impl IntoResponse { match create_session_token( &login_request.auth_token, login_request.remember_me.unwrap_or(false), ) { Ok(session_token) => { let is_https = state.is_https; let cookie = if login_request.remember_me.unwrap_or(false) { // Persistent cookie for remember_me Cookie::build(("session_token", session_token)) .http_only(true) .secure(is_https) .same_site(SameSite::Strict) .path("/") .max_age(time::Duration::weeks(4)) .build() } else { // Session cookie - NO max_age means it expires when browser closes/refreshes Cookie::build(("session_token", session_token)) .http_only(true) .secure(is_https) .same_site(SameSite::Strict) .path("/") .build() }; let mut response = Json(LoginResponse { success: true, message: "Login successful".to_string(), }) .into_response(); if let Ok(cookie_header) = axum::http::HeaderValue::from_str(&cookie.to_string()) { response.headers_mut().insert("set-cookie", cookie_header); } response }, Err(_) => ( StatusCode::UNAUTHORIZED, Json(LoginResponse { success: false, message: "Invalid authentication token".to_string(), }), ) .into_response(), } } pub async fn create_new_client( State(state): State, request: axum::extract::Request, ) -> Result, (StatusCode, impl IntoResponse)> { // Extract is_read_only from request extensions (set by auth middleware) let is_read_only = request .extensions() .get::() .copied() .unwrap_or(IsReadOnly(true)) .0; let session_token_hash = request .extensions() .get::() .cloned() .ok_or(( StatusCode::INTERNAL_SERVER_ERROR, Json("Missing session info".to_string()), ))?; let web_client_id = String::from(Uuid::new_v4()); let os_input = state .client_os_api_factory .create_client_os_api() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string())))?; state.connection_table.lock().unwrap().add_new_client( web_client_id.to_owned(), os_input, is_read_only, session_token_hash.0, ); Ok(Json(CreateClientIdResponse { web_client_id, is_read_only, })) } pub async fn get_static_asset(AxumPath(path): AxumPath) -> impl IntoResponse { let path = path.trim_start_matches('/'); match ASSETS_DIR.get_file(path) { None => ( [(header::CONTENT_TYPE, "text/html")], "Not Found".as_bytes(), ), Some(file) => { let ext = file.path().extension().and_then(|ext| ext.to_str()); let mime_type = get_mime_type(ext); ([(header::CONTENT_TYPE, mime_type)], file.contents()) }, } } pub async fn version_handler() -> &'static str { VERSION }