api.rs

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