Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide the frontend translations via the /po.js path #1126

Merged
merged 2 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/agama-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ utoipa = { version = "4.2.0", features = ["axum_extras"] }
config = "0.14.0"
rand = "0.8.5"
jsonwebtoken = "9.2.0"
axum-extra = { version = "0.9.2", features = ["typed-header"] }
axum-extra = { version = "0.9.2", features = ["cookie", "typed-header"] }
chrono = { version = "0.4.34", default-features = false, features = [
"now",
"std",
Expand Down
66 changes: 63 additions & 3 deletions rust/agama-server/src/web/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ use super::{
state::ServiceState,
};
use axum::{
body::Body,
extract::State,
http::{header::SET_COOKIE, HeaderMap},
http::{header, HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
Json,
};
use axum_extra::extract::cookie::CookieJar;
use pam::Client;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
Expand Down Expand Up @@ -62,7 +64,7 @@ pub async fn login(
let mut headers = HeaderMap::new();
let cookie = format!("agamaToken={}; HttpOnly", &token);
headers.insert(
SET_COOKIE,
header::SET_COOKIE,
cookie.parse().expect("could not build a valid cookie"),
);

Expand All @@ -76,7 +78,7 @@ pub async fn logout(_claims: TokenClaims) -> Result<impl IntoResponse, AuthError
let mut headers = HeaderMap::new();
let cookie = "agamaToken=deleted; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT".to_string();
headers.insert(
SET_COOKIE,
header::SET_COOKIE,
cookie.parse().expect("could not build a valid cookie"),
);
Ok(headers)
Expand All @@ -90,3 +92,61 @@ pub async fn logout(_claims: TokenClaims) -> Result<impl IntoResponse, AuthError
pub async fn session(_claims: TokenClaims) -> Result<(), AuthError> {
Ok(())
}

// builds a response tuple for translation redirection
fn redirect_to_file(file: &str) -> (StatusCode, HeaderMap, Body) {
tracing::info!("Redirecting to translation file {}", file);

let mut response_headers = HeaderMap::new();
// translation found, redirect to the real file
response_headers.insert(
header::LOCATION,
// if the file exists then the name is a valid value and unwrapping is safe
HeaderValue::from_str(file).unwrap(),
);

(
StatusCode::TEMPORARY_REDIRECT,
response_headers,
Body::empty(),
)
}

// handle the /po.js request
// the requested language (locale) is sent in the "agamaLang" HTTP cookie
// this reimplements the Cockpit translation support
pub async fn po(State(state): State<ServiceState>, jar: CookieJar) -> impl IntoResponse {
if let Some(cookie) = jar.get("agamaLang") {
tracing::info!("Language cookie: {}", cookie.value());
// try parsing the cookie
if let Some((lang, region)) = cookie.value().split_once('-') {
// first try language + country
let target_file = format!("po.{}_{}.js", lang, region.to_uppercase());
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
} else {
// then try the language only
let target_file = format!("po.{}.js", lang);
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
};
}
} else {
// use the cookie as is
let target_file = format!("po.{}.js", cookie.value());
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
lslezak marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

tracing::info!("Translation not found");
// fallback, return empty javascript translations if the language is not supported
let mut response_headers = HeaderMap::new();
response_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/javascript"),
);

(StatusCode::OK, response_headers, Body::empty())
}
4 changes: 4 additions & 0 deletions rust/agama-server/src/web/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl MainServiceBuilder {
let state = ServiceState {
config: self.config,
events: self.events,
public_dir: self.public_dir.clone(),
};

let api_router = self
Expand All @@ -84,9 +85,12 @@ impl MainServiceBuilder {
.route("/ping", get(super::http::ping))
.route("/auth", post(login).get(session).delete(logout));

tracing::info!("Serving static files from {}", self.public_dir.display());
let serve = ServeDir::new(self.public_dir);

Router::new()
.nest_service("/", serve)
.route("/po.js", get(super::http::po))
.nest("/api", api_router)
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new().br(true))
Expand Down
2 changes: 2 additions & 0 deletions rust/agama-server/src/web/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Implements the web service state.

use super::{config::ServiceConfig, EventsSender};
use std::path::PathBuf;

/// Web service state.
///
Expand All @@ -9,4 +10,5 @@ use super::{config::ServiceConfig, EventsSender};
pub struct ServiceState {
pub config: ServiceConfig,
pub events: EventsSender,
pub public_dir: PathBuf,
}
Loading