asklyphe/asklyphe-frontend/src/routes/index.rs
2025-03-19 20:58:23 -07:00

267 lines
9.9 KiB
Rust

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<jetstream::Context>) -> Option<FrontpageAnnouncement> {
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<Arc<jetstream::Context>>,
Extension(opts): Extension<Opts>,
) -> 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<String>,
version: String,
git_commit: String,
built_on: String,
year: String,
alpha: bool,
count: u64,
theme: Themes,
announcement: Option<FrontpageAnnouncement>,
}
pub async fn frontpage(
Extension(nats): Extension<Arc<jetstream::Context>>,
Extension(opts): Extension<Opts>) -> 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<FrontpageAnnouncement>,
}
pub async fn index(
jar: CookieJar,
Extension(nats): Extension<Arc<jetstream::Context>>,
Extension(opts): Extension<Opts>,
) -> 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()
}
}