This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 19:05:55 +02:00
1 parent ddc5c6593f
commit b9d137eee1
63 files changed
+1232 -445

No files matched your search

+197
View File
@@ -0,0 +1,197 @@
const LIVE_LOG_COLUMNS: &[&str] = &[
"time",
"request",
"host",
"source_ip",
"vhost",
"backend",
"status",
"duration",
"cache",
"outcome",
"transport",
"websocket",
"upstream_error",
];
const FILE_LOG_COLUMNS: &[&str] = &[
"time", "event", "status", "client_ip", "vhost", "transport", "matched_by", "sni",
"authority", "record", "source",
];
const BACKEND_TABLE_COLUMNS: &[&str] = &[
"vhost",
"backend",
"status",
"active",
"requests",
"errors",
"latency",
"traffic",
"generation",
"generationRequests",
"generationAge",
"rotations",
"lastFailure",
"lastRecovery",
];
fn default_admin_theme() -> String {
"dark".to_owned()
}
fn default_file_log_columns() -> Vec<String> {
["time", "event", "status", "client_ip", "vhost", "record", "source"]
.into_iter()
.map(str::to_owned)
.collect()
}
fn default_backend_table_columns() -> Vec<String> {
BACKEND_TABLE_COLUMNS
.iter()
.map(|value| (*value).to_owned())
.collect()
}
fn default_backend_table_sort() -> String {
"vhost".to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AdminPreferences {
live_log_columns: Vec<String>,
#[serde(default = "default_admin_theme")]
theme: String,
#[serde(default = "default_file_log_columns")]
file_log_columns: Vec<String>,
#[serde(default = "default_backend_table_columns")]
backend_table_columns: Vec<String>,
#[serde(default = "default_backend_table_sort")]
backend_table_sort: String,
#[serde(default)]
backend_table_descending: bool,
}
impl Default for AdminPreferences {
fn default() -> Self {
Self {
live_log_columns: LIVE_LOG_COLUMNS
.iter()
.map(|value| (*value).to_owned())
.collect(),
theme: default_admin_theme(),
file_log_columns: default_file_log_columns(),
backend_table_columns: default_backend_table_columns(),
backend_table_sort: default_backend_table_sort(),
backend_table_descending: false,
}
}
}
fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiError> {
if preferences.live_log_columns.is_empty()
|| preferences.live_log_columns.len() > LIVE_LOG_COLUMNS.len()
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"at least one live-log column must be visible".into(),
));
}
let mut seen = std::collections::HashSet::new();
for column in &preferences.live_log_columns {
if !LIVE_LOG_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid live-log column preference".into(),
));
}
}
if preferences.file_log_columns.is_empty()
|| preferences.file_log_columns.len() > FILE_LOG_COLUMNS.len()
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"at least one stored-log column must be visible".into(),
));
}
let mut seen = std::collections::HashSet::new();
for column in &preferences.file_log_columns {
if !FILE_LOG_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid stored-log column preference".into(),
));
}
}
if !matches!(preferences.theme.as_str(), "dark" | "light") {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration theme preference".into(),
));
}
if preferences.backend_table_columns.is_empty()
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"at least one backend table column must be visible".into(),
));
}
let mut seen = std::collections::HashSet::new();
for column in &preferences.backend_table_columns {
if !BACKEND_TABLE_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid backend table column preference".into(),
));
}
}
if !BACKEND_TABLE_COLUMNS.contains(&preferences.backend_table_sort.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid backend table sort preference".into(),
));
}
Ok(())
}
async fn preferences(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
) -> Result<Json<AdminPreferences>, ApiError> {
let stored = state
.store
.admin_preferences(&session.username)
.await
.map_err(auth::internal)?;
let preferences = match stored {
Some(document) => match serde_json::from_str::<AdminPreferences>(&document) {
Ok(preferences) if validate_admin_preferences(&preferences).is_ok() => preferences,
Ok(_) | Err(_) => {
tracing::warn!(user = %session.username, "invalid admin view preferences ignored");
AdminPreferences::default()
}
},
None => AdminPreferences::default(),
};
Ok(Json(preferences))
}
async fn update_preferences(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
input: Result<Json<AdminPreferences>, axum::extract::rejection::JsonRejection>,
) -> Result<Json<AdminPreferences>, ApiError> {
let Json(preferences) = input.map_err(public::invalid_json)?;
validate_admin_preferences(&preferences)?;
let document = serde_json::to_string(&preferences).map_err(auth::internal)?;
if !state
.store
.save_admin_preferences(&session.username, &document)
.await
.map_err(auth::internal)?
{
return Err(ApiError(
StatusCode::UNAUTHORIZED,
"auth.invalid_credentials".into(),
));
}
Ok(Json(preferences))
}