api.rs

  1use crate::{
  2    auth,
  3    db::{User, UserId},
  4    AppState, Error, Result,
  5};
  6use anyhow::anyhow;
  7use axum::{
  8    body::Body,
  9    extract::{Path, Query},
 10    http::{self, Request, StatusCode},
 11    middleware::{self, Next},
 12    response::IntoResponse,
 13    routing::{get, post, put},
 14    Extension, Json, Router,
 15};
 16use serde::{Deserialize, Serialize};
 17use std::sync::Arc;
 18use tower::ServiceBuilder;
 19
 20pub fn routes(state: Arc<AppState>) -> Router<Body> {
 21    Router::new()
 22        .route("/users", get(get_users).post(create_user))
 23        .route(
 24            "/users/:id",
 25            put(update_user).delete(destroy_user).get(get_user),
 26        )
 27        .route("/users/:id/access_tokens", post(create_access_token))
 28        .layer(
 29            ServiceBuilder::new()
 30                .layer(Extension(state))
 31                .layer(middleware::from_fn(validate_api_token)),
 32        )
 33    // TODO: Compression on API routes?
 34}
 35
 36pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 37    let token = req
 38        .headers()
 39        .get(http::header::AUTHORIZATION)
 40        .and_then(|header| header.to_str().ok())
 41        .ok_or_else(|| {
 42            Error::Http(
 43                StatusCode::BAD_REQUEST,
 44                "missing authorization header".to_string(),
 45            )
 46        })?
 47        .strip_prefix("token ")
 48        .ok_or_else(|| {
 49            Error::Http(
 50                StatusCode::BAD_REQUEST,
 51                "invalid authorization header".to_string(),
 52            )
 53        })?;
 54
 55    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 56
 57    if token != state.api_token {
 58        Err(Error::Http(
 59            StatusCode::UNAUTHORIZED,
 60            "invalid authorization token".to_string(),
 61        ))?
 62    }
 63
 64    Ok::<_, Error>(next.run(req).await)
 65}
 66
 67async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
 68    let users = app.db.get_all_users().await?;
 69    Ok(Json(users))
 70}
 71
 72#[derive(Deserialize)]
 73struct CreateUserParams {
 74    github_login: String,
 75    admin: bool,
 76}
 77
 78async fn create_user(
 79    Json(params): Json<CreateUserParams>,
 80    Extension(app): Extension<Arc<AppState>>,
 81) -> Result<Json<User>> {
 82    let user_id = app
 83        .db
 84        .create_user(&params.github_login, params.admin)
 85        .await?;
 86
 87    let user = app
 88        .db
 89        .get_user_by_id(user_id)
 90        .await?
 91        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
 92
 93    Ok(Json(user))
 94}
 95
 96#[derive(Deserialize)]
 97struct UpdateUserParams {
 98    admin: bool,
 99}
100
101async fn update_user(
102    Path(user_id): Path<i32>,
103    Json(params): Json<UpdateUserParams>,
104    Extension(app): Extension<Arc<AppState>>,
105) -> Result<()> {
106    app.db
107        .set_user_is_admin(UserId(user_id), params.admin)
108        .await?;
109    Ok(())
110}
111
112async fn destroy_user(
113    Path(user_id): Path<i32>,
114    Extension(app): Extension<Arc<AppState>>,
115) -> Result<()> {
116    app.db.destroy_user(UserId(user_id)).await?;
117    Ok(())
118}
119
120async fn get_user(
121    Path(login): Path<String>,
122    Extension(app): Extension<Arc<AppState>>,
123) -> Result<Json<User>> {
124    let user = app
125        .db
126        .get_user_by_github_login(&login)
127        .await?
128        .ok_or_else(|| anyhow!("user not found"))?;
129    Ok(Json(user))
130}
131
132#[derive(Deserialize)]
133struct CreateAccessTokenQueryParams {
134    public_key: String,
135    impersonate: Option<String>,
136}
137
138#[derive(Serialize)]
139struct CreateAccessTokenResponse {
140    user_id: UserId,
141    encrypted_access_token: String,
142}
143
144async fn create_access_token(
145    Path(login): Path<String>,
146    Query(params): Query<CreateAccessTokenQueryParams>,
147    Extension(app): Extension<Arc<AppState>>,
148) -> Result<Json<CreateAccessTokenResponse>> {
149    //     request.require_token().await?;
150
151    let user = app
152        .db
153        .get_user_by_github_login(&login)
154        .await?
155        .ok_or_else(|| anyhow!("user not found"))?;
156
157    let mut user_id = user.id;
158    if let Some(impersonate) = params.impersonate {
159        if user.admin {
160            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
161                user_id = impersonated_user.id;
162            } else {
163                return Err(Error::Http(
164                    StatusCode::UNPROCESSABLE_ENTITY,
165                    format!("user {impersonate} does not exist"),
166                ));
167            }
168        } else {
169            return Err(Error::Http(
170                StatusCode::UNAUTHORIZED,
171                format!("you do not have permission to impersonate other users"),
172            ));
173        }
174    }
175
176    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
177    let encrypted_access_token =
178        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
179
180    Ok(Json(CreateAccessTokenResponse {
181        user_id,
182        encrypted_access_token,
183    }))
184}