OXYGENHUB
# 756fe44a0f4d oxygenhub / crates / hub / src / web / admin.rs
5.1 KiB · Rust · last changed Password reset and email verification: single-use hashed links, reset by verified email or an administrator's link, verification on every address change, notification mail only to verified addresses, and a file:// mail transport for development 1 month ago source blame history raw
1
//! Site administration (hub spec §6.3 `/admin/*`): accounts, registration
2
//! mode and invitations, the audit log.
3
4
use super::settings::csrf;
5
use super::{filters, Error, Page, Shell};
6
use crate::auth::{self, Viewer};
7
use crate::db::accounts;
8
use crate::Shared;
9
use askama::Template;
10
use axum::extract::{Form, State};
11
use serde::Deserialize;
12
13
#[derive(Template)]
14
#[template(path = "admin.html")]
15
pub struct AdminPage {
16
    shell: Shell,
17
    accounts: Vec<accounts::Account>,
18
    orgs: Vec<accounts::Organisation>,
19
    mode: String,
20
    invitations: Vec<(String, Option<i64>, i64)>,
21
    audit: Vec<accounts::AuditRow>,
22
    repos: usize,
23
    notice: Option<String>,
24
}
25
26
fn require_admin(viewer: &Viewer, shell: &Shell) -> Result<accounts::Account, Error> {
27
    match viewer.account() {
28
        Some(a) if a.is_admin => Ok(a.clone()),
29
        _ => Err(Error::not_found(shell.clone())),
30
    }
31
}
32
33
async fn admin_page(app: &Shared, shell: Shell, notice: Option<String>) -> Result<Page<AdminPage>, Error> {
34
    let e = |e| Error::from_db(shell.clone(), e);
35
    let accounts_list = accounts::accounts(&app.db).await.map_err(e)?;
36
    let orgs = accounts::orgs(&app.db).await.map_err(e)?;
37
    let mode = super::session::registration_mode(app).await;
38
    let invitations = accounts::invitations(&app.db).await.map_err(e)?;
39
    let audit = accounts::audit_rows(&app.db, 100).await.map_err(e)?;
40
    let repos = crate::db::repos::list(&app.db).await.map_err(e)?.len();
41
    Ok(Page(AdminPage { shell, accounts: accounts_list, orgs, mode, invitations, audit, repos, notice }))
42
}
43
44
pub async fn index(State(app): State<Shared>, viewer: Viewer) -> Result<Page<AdminPage>, Error> {
45
    let shell = Shell::new(&app, "/admin", &viewer);
46
    require_admin(&viewer, &shell)?;
47
    admin_page(&app, shell, None).await
48
}
49
50
#[derive(Deserialize)]
51
pub struct ModeForm {
52
    #[serde(rename = "_csrf")]
53
    csrf_token: String,
54
    mode: String,
55
}
56
57
pub async fn set_mode(State(app): State<Shared>, viewer: Viewer, Form(f): Form<ModeForm>) -> Result<Page<AdminPage>, Error> {
58
    let shell = Shell::new(&app, "/admin", &viewer);
59
    let me = require_admin(&viewer, &shell)?;
60
    csrf(&viewer, &f.csrf_token, &shell)?;
61
    if !matches!(f.mode.as_str(), "open" | "invite" | "closed") {
62
        return Err(Error::bad(shell, "mode is open, invite or closed"));
63
    }
64
    accounts::set_setting(&app.db, "registration_mode", &f.mode).await.map_err(|e| Error::from_db(shell.clone(), e))?;
65
    accounts::audit(&app.db, Some(&me), "admin.registration", &f.mode, "").await.ok();
66
    admin_page(&app, shell, Some(format!("Registration is {}.", f.mode))).await
67
}
68
69
pub async fn invite(State(app): State<Shared>, viewer: Viewer, Form(f): Form<super::session::CsrfForm>) -> Result<Page<AdminPage>, Error> {
70
    let shell = Shell::new(&app, "/admin", &viewer);
71
    let me = require_admin(&viewer, &shell)?;
72
    csrf(&viewer, &f.csrf, &shell)?;
73
    let code = auth::random_hex(12);
74
    accounts::create_invitation(&app.db, &code, Some(me.id)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
75
    accounts::audit(&app.db, Some(&me), "admin.invite", &code, "").await.ok();
76
    admin_page(&app, shell, Some(format!("Invitation created: {}/register?invite={code}", app.config.base_url()))).await
77
}
78
79
#[derive(Deserialize)]
80
pub struct AccountForm {
81
    #[serde(rename = "_csrf")]
82
    csrf_token: String,
83
    username: String,
84
    action: String,
85
}
86
87
pub async fn account(State(app): State<Shared>, viewer: Viewer, Form(f): Form<AccountForm>) -> Result<Page<AdminPage>, Error> {
88
    let shell = Shell::new(&app, "/admin", &viewer);
89
    let me = require_admin(&viewer, &shell)?;
90
    csrf(&viewer, &f.csrf_token, &shell)?;
91
    let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))? else {
92
        return admin_page(&app, shell, Some("No such account.".into())).await;
93
    };
94
    if target.id == me.id && matches!(f.action.as_str(), "ban" | "demote") {
95
        return admin_page(&app, shell, Some("Not on yourself.".into())).await;
96
    }
97
    let e = |e| Error::from_db(shell.clone(), e);
98
    if f.action == "reset_link" {
99
        if target.banned {
100
            return admin_page(&app, shell, Some("A banned account cannot be reset.".into())).await;
101
        }
102
        let url = super::recovery::admin_reset_link(&app, &me, &target).await.map_err(e)?;
103
        return admin_page(
104
            &app,
105
            shell,
106
            Some(format!("Reset link for {} (works once, for 24 hours; shown only now): {url}", target.username)),
107
        )
108
        .await;
109
    }
110
    match f.action.as_str() {
111
        "ban" => accounts::set_banned(&app.db, target.id, true).await.map_err(e)?,
112
        "unban" => accounts::set_banned(&app.db, target.id, false).await.map_err(e)?,
113
        "promote" => accounts::set_admin(&app.db, target.id, true).await.map_err(e)?,
114
        "demote" => accounts::set_admin(&app.db, target.id, false).await.map_err(e)?,
115
        _ => return Err(Error::bad(shell, "unknown action")),
116
    }
117
    accounts::audit(&app.db, Some(&me), &format!("admin.account.{}", f.action), &target.username, "").await.ok();
118
    admin_page(&app, shell, Some(format!("{}: {}.", target.username, f.action))).await
119
}