working better, small changes

This commit is contained in:
Priec
2026-05-16 12:12:33 +02:00
parent 7d05209d48
commit 164dacb678
7 changed files with 128 additions and 25 deletions

View File

@@ -36,6 +36,23 @@ pub struct AdminAuth {
pub user: users::Model,
}
/// Returns the logged-in admin user if the request carries a valid admin
/// cookie. Unlike the [`AdminAuth`] guard this never rejects, so public pages
/// can detect an admin visitor without redirecting non-admins away.
pub async fn current_admin(ctx: &AppContext, jar: &CookieJar) -> Option<users::Model> {
let admin = admin_email();
if admin.is_empty() {
return None;
}
let token = jar.get(AUTH_COOKIE).map(|c| c.value().to_string())?;
let (secret, _) = jwt_settings(ctx)?;
let claims = jwt::JWT::new(&secret).validate(&token).ok()?;
let user = users::Model::find_by_pid(&ctx.db, &claims.claims.pid)
.await
.ok()?;
(user.email == admin).then_some(user)
}
impl FromRequestParts<AppContext> for AdminAuth {
type Rejection = Redirect;
@@ -43,28 +60,11 @@ impl FromRequestParts<AppContext> for AdminAuth {
parts: &mut Parts,
ctx: &AppContext,
) -> std::result::Result<Self, Self::Rejection> {
let deny = || Redirect::to("/admin/login");
let admin = admin_email();
if admin.is_empty() {
return Err(deny());
}
let jar = CookieJar::from_headers(&parts.headers);
let token = jar
.get(AUTH_COOKIE)
.map(|c| c.value().to_string())
.ok_or_else(deny)?;
let (secret, _) = jwt_settings(ctx).ok_or_else(deny)?;
let claims = jwt::JWT::new(&secret)
.validate(&token)
.map_err(|_| deny())?;
let user = users::Model::find_by_pid(&ctx.db, &claims.claims.pid)
.await
.map_err(|_| deny())?;
if user.email != admin {
return Err(deny());
match current_admin(ctx, &jar).await {
Some(user) => Ok(Self { user }),
None => Err(Redirect::to("/admin/login")),
}
Ok(Self { user })
}
}
@@ -138,18 +138,25 @@ pub async fn dashboard(
Query(q): Query<calendar::CalQuery>,
) -> Result<Response> {
let lang = current_lang(&jar);
let page = build_calendar(&ctx, &lang, true, q.court, q.week).await?;
let page = build_calendar(&ctx, &lang, true, true, q.court, q.week).await?;
format::render().view(&v, "calendar/week.html", &page)
}
// ---------------------------------------------------------------- courts ---
#[derive(Debug, Deserialize)]
pub struct CourtsQuery {
/// Set to `name` when a court-delete confirmation name did not match.
pub err: Option<String>,
}
#[debug_handler]
pub async fn courts_page(
_auth: AdminAuth,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
jar: CookieJar,
Query(q): Query<CourtsQuery>,
) -> Result<Response> {
let lang = current_lang(&jar);
let list = courts::Entity::find()
@@ -170,7 +177,13 @@ pub async fn courts_page(
format::render().view(
&v,
"admin/courts.html",
data!({ "lang": lang, "is_admin": true, "courts": items }),
data!({
"lang": lang,
"is_admin": true,
"logged_in": true,
"courts": items,
"name_error": q.err.as_deref() == Some("name"),
}),
)
}
@@ -198,6 +211,43 @@ pub async fn create_court(
Ok(Redirect::to("/admin/courts").into_response())
}
#[derive(Debug, Deserialize)]
pub struct DeleteCourtForm {
/// The court name the admin retyped to confirm removal.
pub confirm_name: String,
}
/// Removes a court. As a safeguard the admin must retype the court's exact
/// name; a mismatch aborts and redirects back with an error. Deleting a court
/// also removes all of its bookings, since they would otherwise be orphaned.
#[debug_handler]
pub async fn delete_court(
_auth: AdminAuth,
State(ctx): State<AppContext>,
Path(id): Path<i32>,
Form(form): Form<DeleteCourtForm>,
) -> Result<Response> {
let court = courts::Entity::find_by_id(id)
.one(&ctx.db)
.await?
.ok_or(Error::NotFound)?;
let actual = court
.name
.clone()
.unwrap_or_else(|| format!("Court {}", court.id));
if form.confirm_name.trim() != actual {
return Ok(Redirect::to("/admin/courts?err=name").into_response());
}
bookings::Entity::delete_many()
.filter(bookings::Column::CourtId.eq(id))
.exec(&ctx.db)
.await?;
courts::Entity::delete_by_id(id).exec(&ctx.db).await?;
Ok(Redirect::to("/admin/courts").into_response())
}
// --------------------------------------------------------------- bookings --
fn hour_options() -> Vec<serde_json::Value> {
@@ -242,6 +292,7 @@ pub async fn booking_new(
data!({
"lang": lang,
"is_admin": true,
"logged_in": true,
"mode": "new",
"action": "/admin/booking",
"court_id": court_id,
@@ -321,6 +372,7 @@ pub async fn booking_edit(
data!({
"lang": lang,
"is_admin": true,
"logged_in": true,
"mode": "edit",
"action": format!("/admin/booking/{id}"),
"court_id": booking.court_id,
@@ -384,6 +436,7 @@ pub fn routes() -> Routes {
.add("/", get(dashboard))
.add("/courts", get(courts_page))
.add("/courts", post(create_court))
.add("/courts/{id}/delete", post(delete_court))
.add("/booking", get(booking_new))
.add("/booking", post(booking_create))
.add("/booking/{id}", get(booking_edit))