first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-03 16:52:28 +01:00
commit 2eb8aac027
91 files changed
+20139

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
use crate::{
db::{Table, row_bool, row_i64, row_string},
error::{AppError, AppResult},
models::{AccountKind, VacationInput, VacationSummary},
routes,
state::AppState,
validation,
};
use axum::{
Json,
extract::{Path, State},
http::HeaderMap,
};
use axum_extra::extract::cookie::CookieJar;
use std::sync::Arc;
pub async fn get(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
jar: CookieJar,
) -> AppResult<Json<Option<VacationSummary>>> {
let session = routes::session(&state, &jar)?;
let u = validation::email(&path)?;
authorize(&state, &session, &u, false)?;
if !state.config.features.vacation || !state.db.has_table(Table::Vacation).await {
return Ok(Json(None));
}
let t = state.db.table(Table::Vacation);
let af = state.db.cast_text("activefrom");
let au = state.db.cast_text("activeuntil");
let active = state.db.flag("active", "active");
let q=state.db.q(&format!("SELECT email,subject,body,interval_time,{af} AS activefrom,{au} AS activeuntil,{active} FROM {t} WHERE email=?"));
let row = sqlx::query(&q)
.bind(&u)
.fetch_optional(state.db.pool())
.await?;
Ok(Json(row.as_ref().map(map)))
}
pub async fn set(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
jar: CookieJar,
headers: HeaderMap,
Json(input): Json<VacationInput>,
) -> AppResult<Json<serde_json::Value>> {
let session = routes::mutation_session(&state, &jar, &headers)?;
let u = validation::email(&path)?;
authorize(&state, &session, &u, true)?;
if validation::email(&input.email)? != u {
return Err(AppError::bad_request(
"Vacation email does not match mailbox",
));
}
if input.subject.len() > 255 || input.body.len() > 65535 {
return Err(AppError::bad_request("Vacation message is too long"));
}
let (_, domain) = u.rsplit_once('@').unwrap();
let t = state.db.table(Table::Vacation);
let check = state.db.q(&format!("SELECT 1 FROM {t} WHERE email=?"));
let exists = sqlx::query(&check)
.bind(&u)
.fetch_optional(state.db.pool())
.await?
.is_some();
let q = if exists {
state.db.q(&format!("UPDATE {t} SET subject=?,body=?,activefrom=?,activeuntil=?,interval_time=?,modified=CURRENT_TIMESTAMP,active=? WHERE email=?"))
} else {
state.db.q(&format!("INSERT INTO {t} (email,subject,body,activefrom,activeuntil,cache,domain,interval_time,created,modified,active) VALUES (?,?,?,?,?,'',?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,?)"))
};
if exists {
sqlx::query(&q)
.bind(&input.subject)
.bind(&input.body)
.bind(&input.active_from)
.bind(&input.active_until)
.bind(input.interval_time)
.bind(input.active)
.bind(&u)
.execute(state.db.pool())
.await?;
} else {
sqlx::query(&q)
.bind(&u)
.bind(&input.subject)
.bind(&input.body)
.bind(&input.active_from)
.bind(&input.active_until)
.bind(domain)
.bind(input.interval_time)
.bind(input.active)
.execute(state.db.pool())
.await?;
}
state
.db
.log(
&state.config,
&session.username,
domain,
"edit_vacation",
&u,
)
.await;
Ok(Json(serde_json::json!({"ok":true})))
}
pub async fn remove(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
jar: CookieJar,
headers: HeaderMap,
) -> AppResult<Json<serde_json::Value>> {
let session = routes::mutation_session(&state, &jar, &headers)?;
let u = validation::email(&path)?;
authorize(&state, &session, &u, true)?;
let t = state.db.table(Table::Vacation);
let q = state.db.q(&format!("DELETE FROM {t} WHERE email=?"));
sqlx::query(&q).bind(&u).execute(state.db.pool()).await?;
if state.db.has_table(Table::VacationNotification).await {
let n = state.db.table(Table::VacationNotification);
let q = state.db.q(&format!("DELETE FROM {n} WHERE on_vacation=?"));
let _ = sqlx::query(&q).bind(&u).execute(state.db.pool()).await;
}
Ok(Json(serde_json::json!({"ok":true})))
}
fn authorize(state: &AppState, s: &crate::auth::Session, u: &str, write: bool) -> AppResult<()> {
if !state.config.features.vacation {
return Err(AppError::forbidden("Vacation is disabled"));
}
if s.kind == AccountKind::User {
if !state.config.features.vacation_control || s.username != u {
return Err(AppError::forbidden(
"Vacation control is disabled for this user",
));
}
Ok(())
} else {
if write && !state.config.features.vacation_control_admin && !s.role.is_global() {
return Err(AppError::forbidden(
"Vacation control is disabled for domain administrators",
));
}
let (_, d) = u.rsplit_once('@').unwrap();
s.require_domain(d)
}
}
fn map(r: &sqlx::any::AnyRow) -> VacationSummary {
VacationSummary {
email: row_string(r, "email"),
subject: row_string(r, "subject"),
body: row_string(r, "body"),
interval_time: row_i64(r, "interval_time"),
active_from: row_string(r, "activefrom"),
active_until: row_string(r, "activeuntil"),
active: row_bool(r, "active"),
}
}