424 lines
14 KiB
Rust
424 lines
14 KiB
Rust
include!("preferences_columns.rs");
|
|
|
|
fn default_admin_theme() -> String {
|
|
"dark".to_owned()
|
|
}
|
|
fn default_admin_language() -> String {
|
|
"auto".to_owned()
|
|
}
|
|
fn default_admin_ui_scale() -> u16 {
|
|
100
|
|
}
|
|
fn default_admin_time_format() -> String {
|
|
"24h".to_owned()
|
|
}
|
|
fn default_admin_chart_style() -> String {
|
|
"default".to_owned()
|
|
}
|
|
fn default_admin_button_style() -> String {
|
|
"normal".to_owned()
|
|
}
|
|
fn default_admin_table_density() -> String {
|
|
"normal".to_owned()
|
|
}
|
|
fn default_admin_dashboard_density() -> String {
|
|
"compact".to_owned()
|
|
}
|
|
fn default_admin_dashboard_widgets() -> Vec<String> {
|
|
DASHBOARD_WIDGETS
|
|
.iter()
|
|
.map(|value| (*value).to_owned())
|
|
.collect()
|
|
}
|
|
fn default_file_log_sort() -> String { "time".to_owned() }
|
|
fn default_file_log_descending() -> bool { true }
|
|
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()
|
|
}
|
|
fn default_vhost_list_sort() -> String {
|
|
"id".to_owned()
|
|
}
|
|
fn default_docker_vhost_sort() -> String {
|
|
"service".to_owned()
|
|
}
|
|
fn default_redirect_list_sort() -> String {
|
|
"id".to_owned()
|
|
}
|
|
fn default_notfound_list_sort() -> String {
|
|
"id".to_owned()
|
|
}
|
|
fn default_stream_list_sort() -> String {
|
|
"name".to_owned()
|
|
}
|
|
|
|
include!("refresh_preferences.rs");
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct AdminPreferences {
|
|
#[serde(default)]
|
|
refresh: RefreshPreferences,
|
|
#[serde(default = "default_live_log_columns")]
|
|
live_log_columns: Vec<String>,
|
|
#[serde(default = "default_admin_theme")]
|
|
theme: String,
|
|
#[serde(default = "default_admin_language")]
|
|
language: String,
|
|
#[serde(default = "default_admin_ui_scale")]
|
|
ui_scale: u16,
|
|
#[serde(default = "default_admin_time_format")]
|
|
time_format: String,
|
|
#[serde(default = "default_admin_chart_style")]
|
|
chart_style: String,
|
|
#[serde(default = "default_admin_button_style")]
|
|
button_style: String,
|
|
#[serde(default = "default_admin_table_density")]
|
|
table_density: String,
|
|
#[serde(default = "default_admin_dashboard_density")]
|
|
dashboard_density: String,
|
|
#[serde(default = "default_admin_dashboard_widgets")]
|
|
dashboard_widgets: Vec<String>,
|
|
#[serde(default)]
|
|
dashboard_hidden_widgets: Vec<String>,
|
|
#[serde(default = "default_file_log_columns")]
|
|
file_log_columns: Vec<String>,
|
|
#[serde(default = "default_file_log_sort")]
|
|
file_log_sort: String,
|
|
#[serde(default = "default_file_log_descending")]
|
|
file_log_descending: bool,
|
|
#[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,
|
|
#[serde(default = "default_vhost_list_sort")]
|
|
vhost_list_sort: String,
|
|
#[serde(default)]
|
|
vhost_list_descending: bool,
|
|
#[serde(default = "default_docker_vhost_sort")]
|
|
docker_vhost_sort: String,
|
|
#[serde(default)]
|
|
docker_vhost_descending: bool,
|
|
#[serde(default = "default_redirect_list_sort")]
|
|
redirect_list_sort: String,
|
|
#[serde(default)]
|
|
redirect_list_descending: bool,
|
|
#[serde(default = "default_notfound_list_sort")]
|
|
notfound_list_sort: String,
|
|
#[serde(default)]
|
|
notfound_list_descending: bool,
|
|
#[serde(default = "default_stream_list_sort")]
|
|
stream_list_sort: String,
|
|
#[serde(default)]
|
|
stream_list_descending: bool,
|
|
}
|
|
|
|
impl Default for AdminPreferences {
|
|
fn default() -> Self {
|
|
Self {
|
|
refresh: RefreshPreferences::default(),
|
|
live_log_columns: LIVE_LOG_COLUMNS
|
|
.iter()
|
|
.map(|value| (*value).to_owned())
|
|
.collect(),
|
|
theme: default_admin_theme(),
|
|
language: default_admin_language(),
|
|
ui_scale: default_admin_ui_scale(),
|
|
time_format: default_admin_time_format(),
|
|
chart_style: default_admin_chart_style(),
|
|
button_style: default_admin_button_style(),
|
|
table_density: default_admin_table_density(),
|
|
dashboard_density: default_admin_dashboard_density(),
|
|
dashboard_widgets: default_admin_dashboard_widgets(),
|
|
dashboard_hidden_widgets: Vec::new(),
|
|
file_log_columns: default_file_log_columns(),
|
|
file_log_sort: default_file_log_sort(),
|
|
file_log_descending: default_file_log_descending(),
|
|
backend_table_columns: default_backend_table_columns(),
|
|
backend_table_sort: default_backend_table_sort(),
|
|
backend_table_descending: false,
|
|
vhost_list_sort: default_vhost_list_sort(),
|
|
vhost_list_descending: false,
|
|
docker_vhost_sort: default_docker_vhost_sort(),
|
|
docker_vhost_descending: false,
|
|
redirect_list_sort: default_redirect_list_sort(),
|
|
redirect_list_descending: false,
|
|
notfound_list_sort: default_notfound_list_sort(),
|
|
notfound_list_descending: false,
|
|
stream_list_sort: default_stream_list_sort(),
|
|
stream_list_descending: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiError> {
|
|
validate_refresh_preferences(&preferences.refresh)?;
|
|
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_COLUMN_MAX
|
|
{
|
|
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 column.is_empty()
|
|
|| column.len() > 128
|
|
|| column.chars().any(char::is_control)
|
|
|| !seen.insert(column.as_str())
|
|
{
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid stored-log column preference".into(),
|
|
));
|
|
}
|
|
}
|
|
if !valid_raw_field_key(&preferences.file_log_sort) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid stored-log sort column preference".into(),
|
|
));
|
|
}
|
|
if !matches!(preferences.theme.as_str(), "dark" | "light") {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration theme preference".into(),
|
|
));
|
|
}
|
|
if preferences.language != "auto"
|
|
&& (preferences.language.is_empty()
|
|
|| preferences.language.len() > 64
|
|
|| !preferences
|
|
.language
|
|
.chars()
|
|
.all(|value| value.is_ascii_alphanumeric() || matches!(value, '_' | '-')))
|
|
{
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration language preference".into(),
|
|
));
|
|
}
|
|
if !(70..=125).contains(&preferences.ui_scale) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration interface scale preference".into(),
|
|
));
|
|
}
|
|
if !matches!(preferences.time_format.as_str(), "24h" | "12h") {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration time format preference".into(),
|
|
));
|
|
}
|
|
if !matches!(
|
|
preferences.chart_style.as_str(),
|
|
"default"
|
|
| "soft"
|
|
| "technical"
|
|
| "high_contrast"
|
|
| "neon"
|
|
| "monochrome"
|
|
| "warm"
|
|
| "cool"
|
|
) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration chart style preference".into(),
|
|
));
|
|
}
|
|
if !matches!(preferences.button_style.as_str(), "normal" | "compact") {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration button style preference".into(),
|
|
));
|
|
}
|
|
if !matches!(
|
|
preferences.table_density.as_str(),
|
|
"normal" | "compact" | "ultra"
|
|
) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration table density preference".into(),
|
|
));
|
|
}
|
|
if !matches!(
|
|
preferences.dashboard_density.as_str(),
|
|
"normal" | "compact" | "ultra"
|
|
) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration dashboard density preference".into(),
|
|
));
|
|
}
|
|
if preferences.dashboard_widgets.is_empty()
|
|
|| preferences.dashboard_widgets.len() > DASHBOARD_WIDGETS.len()
|
|
{
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration dashboard widget order".into(),
|
|
));
|
|
}
|
|
let mut seen = std::collections::HashSet::new();
|
|
for widget in &preferences.dashboard_widgets {
|
|
if !DASHBOARD_WIDGETS.contains(&widget.as_str()) || !seen.insert(widget.as_str()) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration dashboard widget order".into(),
|
|
));
|
|
}
|
|
}
|
|
if preferences.dashboard_hidden_widgets.len() > DASHBOARD_WIDGETS.len() {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration hidden dashboard widgets".into(),
|
|
));
|
|
}
|
|
let mut seen = std::collections::HashSet::new();
|
|
for widget in &preferences.dashboard_hidden_widgets {
|
|
if !DASHBOARD_WIDGETS.contains(&widget.as_str()) || !seen.insert(widget.as_str()) {
|
|
return Err(ApiError(
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"invalid administration hidden dashboard widgets".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(),
|
|
));
|
|
}
|
|
for (value, allowed, message) in [
|
|
(
|
|
&preferences.vhost_list_sort,
|
|
VHOST_LIST_SORT_COLUMNS,
|
|
"invalid vhost list sort preference",
|
|
),
|
|
(
|
|
&preferences.docker_vhost_sort,
|
|
DOCKER_VHOST_SORT_COLUMNS,
|
|
"invalid Docker vhost sort preference",
|
|
),
|
|
(
|
|
&preferences.redirect_list_sort,
|
|
REDIRECT_LIST_SORT_COLUMNS,
|
|
"invalid redirect list sort preference",
|
|
),
|
|
(
|
|
&preferences.notfound_list_sort,
|
|
NOTFOUND_LIST_SORT_COLUMNS,
|
|
"invalid 404-host sort preference",
|
|
),
|
|
(
|
|
&preferences.stream_list_sort,
|
|
STREAM_LIST_SORT_COLUMNS,
|
|
"invalid stream list sort preference",
|
|
),
|
|
] {
|
|
if !allowed.contains(&value.as_str()) {
|
|
return Err(ApiError(StatusCode::UNPROCESSABLE_ENTITY, message.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))
|
|
}
|
|
|