api.rs

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