admin.rs

  1use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _};
  2use async_trait::async_trait;
  3use serde::{Deserialize, Serialize};
  4use sqlx::{Executor, FromRow};
  5use std::sync::Arc;
  6use surf::http::mime;
  7
  8#[async_trait]
  9pub trait RequestExt {
 10    async fn require_admin(&self) -> tide::Result<()>;
 11}
 12
 13#[async_trait]
 14impl RequestExt for Request {
 15    async fn require_admin(&self) -> tide::Result<()> {
 16        let current_user = self
 17            .current_user()
 18            .await?
 19            .ok_or_else(|| tide::Error::from_str(401, "not logged in"))?;
 20
 21        if current_user.is_admin {
 22            Ok(())
 23        } else {
 24            Err(tide::Error::from_str(
 25                403,
 26                "authenticated user is not an admin",
 27            ))
 28        }
 29    }
 30}
 31
 32pub fn add_routes(app: &mut tide::Server<Arc<AppState>>) {
 33    app.at("/admin").get(get_admin_page);
 34    app.at("/users").post(post_user);
 35    app.at("/users/:id").put(put_user);
 36    app.at("/users/:id/delete").post(delete_user);
 37    app.at("/signups/:id/delete").post(delete_signup);
 38}
 39
 40#[derive(Serialize)]
 41struct AdminData {
 42    #[serde(flatten)]
 43    layout: Arc<LayoutData>,
 44    users: Vec<User>,
 45    signups: Vec<Signup>,
 46}
 47
 48#[derive(Debug, FromRow, Serialize)]
 49pub struct User {
 50    pub id: i32,
 51    pub github_login: String,
 52    pub admin: bool,
 53}
 54
 55#[derive(Debug, FromRow, Serialize)]
 56pub struct Signup {
 57    pub id: i32,
 58    pub github_login: String,
 59    pub email_address: String,
 60    pub about: String,
 61}
 62
 63async fn get_admin_page(mut request: Request) -> tide::Result {
 64    request.require_admin().await?;
 65
 66    let data = AdminData {
 67        layout: request.layout_data().await?,
 68        users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC")
 69            .fetch_all(request.db())
 70            .await?,
 71        signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC")
 72            .fetch_all(request.db())
 73            .await?,
 74    };
 75
 76    Ok(tide::Response::builder(200)
 77        .body(request.state().render_template("admin.hbs", &data)?)
 78        .content_type(mime::HTML)
 79        .build())
 80}
 81
 82async fn post_user(mut request: Request) -> tide::Result {
 83    request.require_admin().await?;
 84
 85    #[derive(Deserialize)]
 86    struct Form {
 87        github_login: String,
 88        #[serde(default)]
 89        admin: bool,
 90    }
 91
 92    let form = request.body_form::<Form>().await?;
 93    let github_login = form
 94        .github_login
 95        .strip_prefix("@")
 96        .unwrap_or(&form.github_login);
 97
 98    if !github_login.is_empty() {
 99        create_user(request.db(), github_login, form.admin).await?;
100    }
101
102    Ok(tide::Redirect::new("/admin").into())
103}
104
105async fn put_user(mut request: Request) -> tide::Result {
106    request.require_admin().await?;
107
108    let user_id = request.param("id")?.parse::<i32>()?;
109
110    #[derive(Deserialize)]
111    struct Body {
112        admin: bool,
113    }
114
115    let body: Body = request.body_json().await?;
116
117    request
118        .db()
119        .execute(
120            sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;")
121                .bind(body.admin)
122                .bind(user_id),
123        )
124        .await?;
125
126    Ok(tide::Response::builder(200).build())
127}
128
129async fn delete_user(request: Request) -> tide::Result {
130    request.require_admin().await?;
131
132    let user_id = request.param("id")?.parse::<i32>()?;
133    request
134        .db()
135        .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id))
136        .await?;
137
138    Ok(tide::Redirect::new("/admin").into())
139}
140
141pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result<i32> {
142    let id: i32 =
143        sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;")
144            .bind(github_login)
145            .bind(admin)
146            .fetch_one(db)
147            .await?;
148    Ok(id)
149}
150
151async fn delete_signup(request: Request) -> tide::Result {
152    request.require_admin().await?;
153    let signup_id = request.param("id")?.parse::<i32>()?;
154    request
155        .db()
156        .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id))
157        .await?;
158
159    Ok(tide::Redirect::new("/admin").into())
160}