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}
 39
 40pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 41    let token = req
 42        .headers()
 43        .get(http::header::AUTHORIZATION)
 44        .and_then(|header| header.to_str().ok())
 45        .ok_or_else(|| {
 46            Error::Http(
 47                StatusCode::BAD_REQUEST,
 48                "missing authorization header".to_string(),
 49            )
 50        })?
 51        .strip_prefix("token ")
 52        .ok_or_else(|| {
 53            Error::Http(
 54                StatusCode::BAD_REQUEST,
 55                "invalid authorization header".to_string(),
 56            )
 57        })?;
 58
 59    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 60
 61    if token != state.api_token {
 62        Err(Error::Http(
 63            StatusCode::UNAUTHORIZED,
 64            "invalid authorization token".to_string(),
 65        ))?
 66    }
 67
 68    Ok::<_, Error>(next.run(req).await)
 69}
 70
 71async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
 72    let users = app.db.get_all_users().await?;
 73    Ok(Json(users))
 74}
 75
 76#[derive(Deserialize, Debug)]
 77struct CreateUserParams {
 78    github_login: String,
 79    invite_code: Option<String>,
 80    email_address: 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    println!("{:?}", params);
 90
 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) -> Result<()> {
135    if let Some(admin) = params.admin {
136        app.db.set_user_is_admin(UserId(user_id), admin).await?;
137    }
138
139    if let Some(invite_count) = params.invite_count {
140        app.db
141            .set_invite_count(UserId(user_id), invite_count)
142            .await?;
143    }
144
145    Ok(())
146}
147
148async fn destroy_user(
149    Path(user_id): Path<i32>,
150    Extension(app): Extension<Arc<AppState>>,
151) -> Result<()> {
152    app.db.destroy_user(UserId(user_id)).await?;
153    Ok(())
154}
155
156async fn get_user(
157    Path(login): Path<String>,
158    Extension(app): Extension<Arc<AppState>>,
159) -> Result<Json<User>> {
160    let user = app
161        .db
162        .get_user_by_github_login(&login)
163        .await?
164        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
165    Ok(Json(user))
166}
167
168#[derive(Debug, Deserialize)]
169struct Panic {
170    version: String,
171    text: String,
172}
173
174#[instrument(skip(panic))]
175async fn trace_panic(panic: Json<Panic>) -> Result<()> {
176    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
177    Ok(())
178}
179
180#[derive(Deserialize)]
181struct CreateAccessTokenQueryParams {
182    public_key: String,
183    impersonate: Option<String>,
184}
185
186#[derive(Serialize)]
187struct CreateAccessTokenResponse {
188    user_id: UserId,
189    encrypted_access_token: String,
190}
191
192async fn create_access_token(
193    Path(login): Path<String>,
194    Query(params): Query<CreateAccessTokenQueryParams>,
195    Extension(app): Extension<Arc<AppState>>,
196) -> Result<Json<CreateAccessTokenResponse>> {
197    //     request.require_token().await?;
198
199    let user = app
200        .db
201        .get_user_by_github_login(&login)
202        .await?
203        .ok_or_else(|| anyhow!("user not found"))?;
204
205    let mut user_id = user.id;
206    if let Some(impersonate) = params.impersonate {
207        if user.admin {
208            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
209                user_id = impersonated_user.id;
210            } else {
211                return Err(Error::Http(
212                    StatusCode::UNPROCESSABLE_ENTITY,
213                    format!("user {impersonate} does not exist"),
214                ));
215            }
216        } else {
217            return Err(Error::Http(
218                StatusCode::UNAUTHORIZED,
219                format!("you do not have permission to impersonate other users"),
220            ));
221        }
222    }
223
224    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
225    let encrypted_access_token =
226        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
227
228    Ok(Json(CreateAccessTokenResponse {
229        user_id,
230        encrypted_access_token,
231    }))
232}
233
234async fn get_user_for_invite_code(
235    Path(code): Path<String>,
236    Extension(app): Extension<Arc<AppState>>,
237) -> Result<Json<User>> {
238    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
239}