//! Site administration (hub spec ยง6.3 `/admin/*`): accounts, registration //! mode and invitations, the audit log. use super::settings::csrf; use super::{filters, Error, Page, Shell}; use crate::auth::{self, Viewer}; use crate::db::accounts; use crate::Shared; use askama::Template; use axum::extract::{Form, State}; use serde::Deserialize; #[derive(Template)] #[template(path = "admin.html")] pub struct AdminPage { shell: Shell, accounts: Vec, orgs: Vec, mode: String, invitations: Vec<(String, Option, i64)>, audit: Vec, repos: usize, notice: Option, } fn require_admin(viewer: &Viewer, shell: &Shell) -> Result { match viewer.account() { Some(a) if a.is_admin => Ok(a.clone()), _ => Err(Error::not_found(shell.clone())), } } async fn admin_page(app: &Shared, shell: Shell, notice: Option) -> Result, Error> { let e = |e| Error::from_db(shell.clone(), e); let accounts_list = accounts::accounts(&app.db).await.map_err(e)?; let orgs = accounts::orgs(&app.db).await.map_err(e)?; let mode = super::session::registration_mode(app).await; let invitations = accounts::invitations(&app.db).await.map_err(e)?; let audit = accounts::audit_rows(&app.db, 100).await.map_err(e)?; let repos = crate::db::repos::list(&app.db).await.map_err(e)?.len(); Ok(Page(AdminPage { shell, accounts: accounts_list, orgs, mode, invitations, audit, repos, notice })) } pub async fn index(State(app): State, viewer: Viewer) -> Result, Error> { let shell = Shell::new(&app, "/admin", &viewer); require_admin(&viewer, &shell)?; admin_page(&app, shell, None).await } #[derive(Deserialize)] pub struct ModeForm { #[serde(rename = "_csrf")] csrf_token: String, mode: String, } pub async fn set_mode(State(app): State, viewer: Viewer, Form(f): Form) -> Result, Error> { let shell = Shell::new(&app, "/admin", &viewer); let me = require_admin(&viewer, &shell)?; csrf(&viewer, &f.csrf_token, &shell)?; if !matches!(f.mode.as_str(), "open" | "invite" | "closed") { return Err(Error::bad(shell, "mode is open, invite or closed")); } accounts::set_setting(&app.db, "registration_mode", &f.mode).await.map_err(|e| Error::from_db(shell.clone(), e))?; accounts::audit(&app.db, Some(&me), "admin.registration", &f.mode, "").await.ok(); admin_page(&app, shell, Some(format!("Registration is {}.", f.mode))).await } pub async fn invite(State(app): State, viewer: Viewer, Form(f): Form) -> Result, Error> { let shell = Shell::new(&app, "/admin", &viewer); let me = require_admin(&viewer, &shell)?; csrf(&viewer, &f.csrf, &shell)?; let code = auth::random_hex(12); accounts::create_invitation(&app.db, &code, Some(me.id)).await.map_err(|e| Error::from_db(shell.clone(), e))?; accounts::audit(&app.db, Some(&me), "admin.invite", &code, "").await.ok(); admin_page(&app, shell, Some(format!("Invitation created: {}/register?invite={code}", app.config.base_url()))).await } #[derive(Deserialize)] pub struct AccountForm { #[serde(rename = "_csrf")] csrf_token: String, username: String, action: String, } pub async fn account(State(app): State, viewer: Viewer, Form(f): Form) -> Result, Error> { let shell = Shell::new(&app, "/admin", &viewer); let me = require_admin(&viewer, &shell)?; csrf(&viewer, &f.csrf_token, &shell)?; let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))? else { return admin_page(&app, shell, Some("No such account.".into())).await; }; if target.id == me.id && matches!(f.action.as_str(), "ban" | "demote") { return admin_page(&app, shell, Some("Not on yourself.".into())).await; } let e = |e| Error::from_db(shell.clone(), e); match f.action.as_str() { "ban" => accounts::set_banned(&app.db, target.id, true).await.map_err(e)?, "unban" => accounts::set_banned(&app.db, target.id, false).await.map_err(e)?, "promote" => accounts::set_admin(&app.db, target.id, true).await.map_err(e)?, "demote" => accounts::set_admin(&app.db, target.id, false).await.map_err(e)?, _ => return Err(Error::bad(shell, "unknown action")), } accounts::audit(&app.db, Some(&me), &format!("admin.account.{}", f.action), &target.username, "").await.ok(); admin_page(&app, shell, Some(format!("{}: {}.", target.username, f.action))).await }