Files
PostfixAdminRS/src/routes/system.rs
T
2026-11-04 08:55:53 +01:00

148 lines
6.7 KiB
Rust

use crate::{
db::{Table, row_i64},
error::AppResult,
passwords, routes,
state::AppState,
};
use axum::{Json, extract::State};
use axum_extra::extract::cookie::CookieJar;
use serde::Serialize;
use std::sync::Arc;
#[derive(Serialize)]
pub struct Health {
status: &'static str,
version: &'static str,
}
pub async fn health(State(state): State<Arc<AppState>>) -> AppResult<Json<Health>> {
state.db.ping().await?;
Ok(Json(Health {
status: "ok",
version: env!("CARGO_PKG_VERSION"),
}))
}
pub async fn overview(
State(state): State<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let s = routes::session(&state, &jar).await?;
if !s.role.is_admin() {
return Ok(Json(serde_json::json!({
"role": s.role,
"username": s.username,
})));
}
let scope: Option<Vec<String>> = if s.role.is_global() {
None
} else {
Some(routes::scoped_domains(&s))
};
let (in_clause, binds) = match &scope {
Some(domains) if domains.is_empty() => {
return Ok(Json(serde_json::json!({
"domains": 0, "mailboxes": 0, "aliases": 0, "alias_domains": 0,
"role": s.role, "username": s.username,
})));
}
Some(domains) => (
format!(" AND {{col}} IN ({})", vec!["?"; domains.len()].join(",")),
domains.clone(),
),
None => (String::new(), Vec::new()),
};
let db = &state.db;
let domain = db.table(Table::Domain);
let mailbox = db.table(Table::Mailbox);
let alias = db.table(Table::Alias);
let mut counts = serde_json::Map::new();
let queries = vec![
(
"domains",
format!(
"SELECT COUNT(*) AS n FROM {domain} d WHERE d.domain <> 'ALL'{}",
in_clause.replace("{col}", "d.domain")
),
),
(
"mailboxes",
format!(
"SELECT COUNT(*) AS n FROM {mailbox} m WHERE 1=1{}",
in_clause.replace("{col}", "m.domain")
),
),
(
"aliases",
format!(
"SELECT COUNT(*) AS n FROM {alias} a LEFT JOIN {mailbox} m ON m.username=a.address WHERE m.username IS NULL{}",
in_clause.replace("{col}", "a.domain")
),
),
];
for (key, sql) in queries {
let sql = db.q(&sql);
let mut q = sqlx::query(&sql);
for b in &binds {
q = q.bind(b.as_str());
}
counts.insert(
key.into(),
row_i64(&q.fetch_one(db.pool()).await?, "n").into(),
);
}
let alias_domains =
if state.config.features.alias_domain && db.has_table(Table::AliasDomain).await {
let ad = db.table(Table::AliasDomain);
let sql = db.q(&format!(
"SELECT COUNT(*) AS n FROM {ad} x WHERE 1=1{}",
in_clause.replace("{col}", "x.target_domain")
));
let mut q = sqlx::query(&sql);
for b in &binds {
q = q.bind(b.as_str());
}
row_i64(&q.fetch_one(db.pool()).await?, "n")
} else {
0
};
counts.insert("alias_domains".into(), alias_domains.into());
counts.insert(
"role".into(),
serde_json::to_value(s.role).unwrap_or_default(),
);
counts.insert("username".into(), s.username.clone().into());
Ok(Json(serde_json::Value::Object(counts)))
}
pub async fn schema(
State(state): State<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let s = routes::session(&state, &jar).await?;
s.require_global()?;
let warnings = state.db.check_schema(&state.config).await?;
Ok(Json(
serde_json::json!({"ok":warnings.is_empty(),"warnings":warnings,"database_backend":state.config.database.backend}),
))
}
pub async fn features(
State(state): State<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let s = routes::session(&state, &jar).await?;
let f = &state.config.features;
let domain = s.username.rsplit_once('@').map(|(_, d)| d).unwrap_or("");
let permissions = serde_json::json!({
"edit_own_name": s.kind == crate::models::AccountKind::User && f.edit_mailbox_name.allows(domain),
"edit_own_forwarding": s.kind == crate::models::AccountKind::User && f.edit_alias,
"edit_own_vacation": s.kind == crate::models::AccountKind::User && f.vacation && f.vacation_control,
"edit_mailbox_forwarding": s.role.is_admin() && if s.role.is_global() { f.alias_control } else { f.alias_control_admin },
"edit_vacation": s.role.is_admin() && f.vacation && (s.role.is_global() || f.vacation_control_admin),
"manage_domains": s.role.is_global(),
"manage_admins": s.role.is_global(),
"edit_special_aliases": s.role.is_global() || f.special_alias_control,
"sendmail": s.role.is_admin() && f.sendmail && (s.role.is_global() || f.sendmail_all_admins),
"backup": s.role.is_global() && f.backup,
});
Ok(Json(
serde_json::json!({"permissions":permissions,"features":&state.config.features,"defaults":{"page_size":state.config.defaults.page_size,"quota_multiplier":state.config.defaults.quota_multiplier,"quota_level_med_pct":state.config.defaults.quota_level_med_pct,"quota_level_high_pct":state.config.defaults.quota_level_high_pct,"motd_user":state.config.defaults.motd_user,"motd_admin":state.config.defaults.motd_admin,"motd_superadmin":state.config.defaults.motd_superadmin,"vacation_choice_of_reply":state.config.defaults.vacation_choice_of_reply,"transport_options":state.config.defaults.transport_options,"transport_default":state.config.defaults.transport_default,"default_aliases":state.config.defaults.default_aliases.keys().collect::<Vec<_>>(),"password_min_length":state.config.password.min_length,"password_min_letters":state.config.password.min_letters,"password_min_digits":state.config.password.min_digits,"mailbox_postpassword_hook":!state.config.hooks.mailbox_postpassword.is_empty()},"password":{"configured":state.config.password.scheme,"supported":passwords::supported_schemes()},"development_mock":state.config.database.dev_mock,"database_read_only":state.config.database.read_only,"dkim":{"default_backend":state.config.dkim.default_backend,"user_read_only":state.config.dkim.user_read_only},"rspamd_dkim":{"enabled":state.config.rspamd_dkim.enabled,"default_selector":state.config.rspamd_dkim.default_selector,"development_mock":state.config.rspamd_dkim.dev_mock},"sieve":{"enabled":state.config.sieve.enabled,"port":state.config.sieve.port,"tls_mode":state.config.sieve.tls_mode,"read_only":state.config.sieve.read_only},"version":env!("CARGO_PKG_VERSION")}),
))
}