/* * asklyphe-frontend routes/index.rs * - http routes for homepages * * Copyright (C) 2025 Real Microsoft, LLC * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use std::sync::Arc; use std::sync::atomic::Ordering; use askama::Template; use askama_axum::IntoResponse; use asklyphe_common::nats::authservice::{AuthError, AuthRequest, AuthResponse, AuthServiceQuery, AuthServiceRequest, AuthServiceResponse, LogoutRequest, LogoutResponse}; use asklyphe_common::nats::authservice::admin::{Stats, StatsRequest, StatsResponse}; use asklyphe_common::nats::authservice::announcements::{LatestAnnouncementRequest, LatestAnnouncementResponse}; use asklyphe_common::nats::authservice::profile::{UserInfoRequest, UserInfoResponse}; use asklyphe_common::nats::comms; use asklyphe_common::nats::comms::{ServiceError, ServiceResponse}; use async_nats::jetstream; use axum::Extension; use axum::response::Redirect; use axum_extra::extract::CookieJar; use serde::Serialize; use tokio::sync::Mutex; use tracing::error; use tracing::log::warn; use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR}; use crate::routes::{authenticate_user, Themes, UserInfo}; #[derive(Serialize, Debug)] pub struct FrontpageAnnouncement { pub url: String, pub title: String, pub content: String, pub date: String, } async fn latest_announcement(nats: Arc) -> Option { let response = comms::query_service( comms::Query::AuthService(AuthServiceQuery { request: AuthServiceRequest::LatestAnnouncementRequest(LatestAnnouncementRequest {}), replyto: "".to_string(), }), &nats, false, ).await; if let Err(e) = response { error!("nats error: {:?}", e); None } else { let response = response.unwrap(); match response { ServiceResponse::SearchService(_) => { error!("sent search service response when asking for auth service!! investigate ASAP!!!"); None } ServiceResponse::BingService(_) => { error!("sent bing service response when asking for auth service!! investigate ASAP!!!"); None } ServiceResponse::AuthService(r) => match r { AuthServiceResponse::LatestAnnouncementResponse(v) => { match v { LatestAnnouncementResponse::Some(v) => { Some(FrontpageAnnouncement { url: format!("/announcements/{}", v.slug), title: v.title, content: v.short_text, date: chrono::DateTime::from_timestamp(v.date, 0).unwrap().to_rfc2822(), }) } LatestAnnouncementResponse::None => { None } LatestAnnouncementResponse::InternalServerError(e) => { warn!("internal server error during latest announcement request! {e}"); None } } } x => { error!("auth service gave {} to our user info request!", x); None } }, } } } pub fn frontpage_error(error: &str, auth_url: String) -> FrontpageTemplate { FrontpageTemplate { authurl: auth_url, error: Some(error.to_string()), version: VERSION.to_string(), git_commit: GIT_COMMIT.to_string(), built_on: BUILT_ON.to_string(), year: YEAR.to_string(), alpha: ALPHA, count: WEBSITE_COUNT.load(Ordering::Relaxed), theme: Themes::Default, announcement: None, } } pub async fn logout( jar: CookieJar, Extension(nats): Extension>, Extension(opts): Extension, ) -> impl IntoResponse { if let Some(token) = jar.get("token") { let token = token.value().to_string(); let response = comms::query_service( comms::Query::AuthService(AuthServiceQuery { request: AuthServiceRequest::LogoutRequest(LogoutRequest { token, }), replyto: "".to_string(), }), &nats, false, ).await; if let Err(e) = response { error!("nats error: {:?}", e); frontpage_error("internal server error", opts.auth_url.clone()).into_response() } else { let response = response.unwrap(); let mut internal_server_error = false; match &response { ServiceResponse::SearchService(_) => { error!("sent search service response when asking for auth service!! investigate ASAP!!!"); internal_server_error = true; } ServiceResponse::BingService(_) => { error!("sent bing service response when asking for auth service!! investigate ASAP!!!"); internal_server_error = true; } ServiceResponse::AuthService(r) => match r { AuthServiceResponse::LogoutResponse(_) => {} x => { error!("auth service gave {} to our user info request!", x); internal_server_error = true; } } } if internal_server_error { frontpage_error("internal server error", opts.auth_url.clone()).into_response() } else { match response { ServiceResponse::AuthService(AuthServiceResponse::LogoutResponse(r)) => match r { LogoutResponse::Success => { (jar.remove("token"), Redirect::to("/").into_response()).into_response() } LogoutResponse::InternalServerError(e) => { error!("internal server error {e} returned from auth service during logout! investigate ASAP!!!"); frontpage_error("internal server error", opts.auth_url.clone()).into_response() } LogoutResponse::BadTokenError => { (jar.remove("token"), Redirect::to("/").into_response()).into_response() } } _ => unreachable!() } } } } else { Redirect::to("/").into_response() } } #[derive(Template)] #[template(path = "frontpage.html")] pub struct FrontpageTemplate { authurl: String, error: Option, version: String, git_commit: String, built_on: String, year: String, alpha: bool, count: u64, theme: Themes, announcement: Option, } pub async fn frontpage( Extension(nats): Extension>, Extension(opts): Extension) -> impl IntoResponse { let announcement = latest_announcement(nats.clone()).await; FrontpageTemplate { authurl: opts.auth_url.clone(), error: None, version: VERSION.to_string(), git_commit: GIT_COMMIT.to_string(), built_on: BUILT_ON.to_string(), year: YEAR.to_string(), alpha: ALPHA, count: WEBSITE_COUNT.load(Ordering::Relaxed), theme: Themes::Default, announcement, } } #[derive(Template)] #[template(path = "home.html")] struct IndexTemplate { info: UserInfo, version: String, git_commit: String, built_on: String, year: String, alpha: bool, count: u64, theme: Themes, announcement: Option, } pub async fn index( jar: CookieJar, Extension(nats): Extension>, Extension(opts): Extension, ) -> impl IntoResponse { if let Some(token) = jar.get("token") { let token = token.value().to_string(); let info = match authenticate_user(nats.clone(), token).await { Ok(i) => i, Err(e) => { return (jar.remove("token"), frontpage_error(e.as_str(), opts.auth_url.clone())).into_response(); } }; let theme = info.get_theme(); let announcement = latest_announcement(nats.clone()).await; IndexTemplate { info, version: VERSION.to_string(), git_commit: GIT_COMMIT.to_string(), built_on: BUILT_ON.to_string(), year: YEAR.to_string(), alpha: ALPHA, count: WEBSITE_COUNT.load(Ordering::Relaxed), theme, announcement, }.into_response() } else { let announcement = latest_announcement(nats.clone()).await; FrontpageTemplate { authurl: opts.auth_url.clone(), error: None, version: VERSION.to_string(), git_commit: GIT_COMMIT.to_string(), built_on: BUILT_ON.to_string(), year: YEAR.to_string(), alpha: ALPHA, count: WEBSITE_COUNT.load(Ordering::Relaxed), theme: Themes::Default, announcement, }.into_response() } }