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
 73#[derive(Debug, Deserialize)]
 74struct GetUsersQueryParams {
 75    query: Option<String>,
 76    page: Option<u32>,
 77    limit: Option<u32>,
 78}
 79
 80async fn get_users(
 81    Query(params): Query<GetUsersQueryParams>,
 82    Extension(app): Extension<Arc<AppState>>,
 83) -> Result<Json<Vec<User>>> {
 84    let limit = params.limit.unwrap_or(100);
 85    let users = if let Some(query) = params.query {
 86        app.db.fuzzy_search_users(&query, limit).await?
 87    } else {
 88        app.db
 89            .get_all_users(params.page.unwrap_or(0), limit)
 90            .await?
 91    };
 92    Ok(Json(users))
 93}
 94
 95#[derive(Deserialize, Debug)]
 96struct CreateUserParams {
 97    github_login: String,
 98    invite_code: Option<String>,
 99    email_address: Option<String>,
100    admin: bool,
101}
102
103async fn create_user(
104    Json(params): Json<CreateUserParams>,
105    Extension(app): Extension<Arc<AppState>>,
106    Extension(rpc_server): Extension<Arc<rpc::Server>>,
107) -> Result<Json<User>> {
108    let user_id = if let Some(invite_code) = params.invite_code {
109        let invitee_id = app
110            .db
111            .redeem_invite_code(
112                &invite_code,
113                &params.github_login,
114                params.email_address.as_deref(),
115            )
116            .await?;
117        rpc_server
118            .invite_code_redeemed(&invite_code, invitee_id)
119            .await
120            .trace_err();
121        invitee_id
122    } else {
123        app.db
124            .create_user(
125                &params.github_login,
126                params.email_address.as_deref(),
127                params.admin,
128            )
129            .await?
130    };
131
132    let user = app
133        .db
134        .get_user_by_id(user_id)
135        .await?
136        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
137
138    Ok(Json(user))
139}
140
141#[derive(Deserialize)]
142struct UpdateUserParams {
143    admin: Option<bool>,
144    invite_count: Option<u32>,
145}
146
147async fn update_user(
148    Path(user_id): Path<i32>,
149    Json(params): Json<UpdateUserParams>,
150    Extension(app): Extension<Arc<AppState>>,
151    Extension(rpc_server): Extension<Arc<rpc::Server>>,
152) -> Result<()> {
153    let user_id = UserId(user_id);
154
155    if let Some(admin) = params.admin {
156        app.db.set_user_is_admin(user_id, admin).await?;
157    }
158
159    if let Some(invite_count) = params.invite_count {
160        app.db.set_invite_count(user_id, invite_count).await?;
161        rpc_server.invite_count_updated(user_id).await.trace_err();
162    }
163
164    Ok(())
165}
166
167async fn destroy_user(
168    Path(user_id): Path<i32>,
169    Extension(app): Extension<Arc<AppState>>,
170) -> Result<()> {
171    app.db.destroy_user(UserId(user_id)).await?;
172    Ok(())
173}
174
175async fn get_user(
176    Path(login): Path<String>,
177    Extension(app): Extension<Arc<AppState>>,
178) -> Result<Json<User>> {
179    let user = app
180        .db
181        .get_user_by_github_login(&login)
182        .await?
183        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
184    Ok(Json(user))
185}
186
187#[derive(Debug, Deserialize)]
188struct Panic {
189    version: String,
190    text: String,
191}
192
193#[instrument(skip(panic))]
194async fn trace_panic(panic: Json<Panic>) -> Result<()> {
195    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
196    Ok(())
197}
198
199async fn get_rpc_server_snapshot(
200    Extension(rpc_server): Extension<Arc<rpc::Server>>,
201) -> Result<ErasedJson> {
202    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
203}
204
205#[derive(Deserialize)]
206struct CreateAccessTokenQueryParams {
207    public_key: String,
208    impersonate: Option<String>,
209}
210
211#[derive(Serialize)]
212struct CreateAccessTokenResponse {
213    user_id: UserId,
214    encrypted_access_token: String,
215}
216
217async fn create_access_token(
218    Path(login): Path<String>,
219    Query(params): Query<CreateAccessTokenQueryParams>,
220    Extension(app): Extension<Arc<AppState>>,
221) -> Result<Json<CreateAccessTokenResponse>> {
222    //     request.require_token().await?;
223
224    let user = app
225        .db
226        .get_user_by_github_login(&login)
227        .await?
228        .ok_or_else(|| anyhow!("user not found"))?;
229
230    let mut user_id = user.id;
231    if let Some(impersonate) = params.impersonate {
232        if user.admin {
233            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
234                user_id = impersonated_user.id;
235            } else {
236                return Err(Error::Http(
237                    StatusCode::UNPROCESSABLE_ENTITY,
238                    format!("user {impersonate} does not exist"),
239                ));
240            }
241        } else {
242            return Err(Error::Http(
243                StatusCode::UNAUTHORIZED,
244                format!("you do not have permission to impersonate other users"),
245            ));
246        }
247    }
248
249    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
250    let encrypted_access_token =
251        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
252
253    Ok(Json(CreateAccessTokenResponse {
254        user_id,
255        encrypted_access_token,
256    }))
257}
258
259async fn get_user_for_invite_code(
260    Path(code): Path<String>,
261    Extension(app): Extension<Arc<AppState>>,
262) -> Result<Json<User>> {
263    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
264}