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("/bulk_users", post(create_users))
 32        .route("/invite_codes/:code", get(get_user_for_invite_code))
 33        .route("/panic", post(trace_panic))
 34        .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
 35        .layer(
 36            ServiceBuilder::new()
 37                .layer(Extension(state))
 38                .layer(Extension(rpc_server.clone()))
 39                .layer(middleware::from_fn(validate_api_token)),
 40        )
 41}
 42
 43pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 44    let token = req
 45        .headers()
 46        .get(http::header::AUTHORIZATION)
 47        .and_then(|header| header.to_str().ok())
 48        .ok_or_else(|| {
 49            Error::Http(
 50                StatusCode::BAD_REQUEST,
 51                "missing authorization header".to_string(),
 52            )
 53        })?
 54        .strip_prefix("token ")
 55        .ok_or_else(|| {
 56            Error::Http(
 57                StatusCode::BAD_REQUEST,
 58                "invalid authorization header".to_string(),
 59            )
 60        })?;
 61
 62    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 63
 64    if token != state.api_token {
 65        Err(Error::Http(
 66            StatusCode::UNAUTHORIZED,
 67            "invalid authorization token".to_string(),
 68        ))?
 69    }
 70
 71    Ok::<_, Error>(next.run(req).await)
 72}
 73
 74#[derive(Debug, Deserialize)]
 75struct GetUsersQueryParams {
 76    query: Option<String>,
 77    page: Option<u32>,
 78    limit: Option<u32>,
 79}
 80
 81async fn get_users(
 82    Query(params): Query<GetUsersQueryParams>,
 83    Extension(app): Extension<Arc<AppState>>,
 84) -> Result<Json<Vec<User>>> {
 85    let limit = params.limit.unwrap_or(100);
 86    let users = if let Some(query) = params.query {
 87        app.db.fuzzy_search_users(&query, limit).await?
 88    } else {
 89        app.db
 90            .get_all_users(params.page.unwrap_or(0), limit)
 91            .await?
 92    };
 93    Ok(Json(users))
 94}
 95
 96#[derive(Deserialize, Debug)]
 97struct CreateUserParams {
 98    github_login: String,
 99    invite_code: Option<String>,
100    email_address: Option<String>,
101    admin: bool,
102}
103
104async fn create_user(
105    Json(params): Json<CreateUserParams>,
106    Extension(app): Extension<Arc<AppState>>,
107    Extension(rpc_server): Extension<Arc<rpc::Server>>,
108) -> Result<Json<User>> {
109    let user_id = if let Some(invite_code) = params.invite_code {
110        let invitee_id = app
111            .db
112            .redeem_invite_code(
113                &invite_code,
114                &params.github_login,
115                params.email_address.as_deref(),
116            )
117            .await?;
118        rpc_server
119            .invite_code_redeemed(&invite_code, invitee_id)
120            .await
121            .trace_err();
122        invitee_id
123    } else {
124        app.db
125            .create_user(
126                &params.github_login,
127                params.email_address.as_deref(),
128                params.admin,
129            )
130            .await?
131    };
132
133    let user = app
134        .db
135        .get_user_by_id(user_id)
136        .await?
137        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
138
139    Ok(Json(user))
140}
141
142#[derive(Deserialize)]
143struct UpdateUserParams {
144    admin: Option<bool>,
145    invite_count: Option<u32>,
146}
147
148async fn update_user(
149    Path(user_id): Path<i32>,
150    Json(params): Json<UpdateUserParams>,
151    Extension(app): Extension<Arc<AppState>>,
152    Extension(rpc_server): Extension<Arc<rpc::Server>>,
153) -> Result<()> {
154    let user_id = UserId(user_id);
155
156    if let Some(admin) = params.admin {
157        app.db.set_user_is_admin(user_id, admin).await?;
158    }
159
160    if let Some(invite_count) = params.invite_count {
161        app.db.set_invite_count(user_id, invite_count).await?;
162        rpc_server.invite_count_updated(user_id).await.trace_err();
163    }
164
165    Ok(())
166}
167
168async fn destroy_user(
169    Path(user_id): Path<i32>,
170    Extension(app): Extension<Arc<AppState>>,
171) -> Result<()> {
172    app.db.destroy_user(UserId(user_id)).await?;
173    Ok(())
174}
175
176async fn get_user(
177    Path(login): Path<String>,
178    Extension(app): Extension<Arc<AppState>>,
179) -> Result<Json<User>> {
180    let user = app
181        .db
182        .get_user_by_github_login(&login)
183        .await?
184        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
185    Ok(Json(user))
186}
187
188#[derive(Deserialize)]
189struct CreateUsersParams {
190    users: Vec<CreateUsersEntry>,
191}
192
193#[derive(Deserialize)]
194struct CreateUsersEntry {
195    github_login: String,
196    email_address: String,
197    invite_count: usize,
198}
199
200async fn create_users(
201    Json(params): Json<CreateUsersParams>,
202    Extension(app): Extension<Arc<AppState>>,
203) -> Result<Json<Vec<User>>> {
204    let user_ids = app
205        .db
206        .create_users(
207            params
208                .users
209                .into_iter()
210                .map(|params| {
211                    (
212                        params.github_login,
213                        params.email_address,
214                        params.invite_count,
215                    )
216                })
217                .collect(),
218        )
219        .await?;
220    let users = app.db.get_users_by_ids(user_ids).await?;
221    Ok(Json(users))
222}
223
224#[derive(Debug, Deserialize)]
225struct Panic {
226    version: String,
227    text: String,
228}
229
230#[instrument(skip(panic))]
231async fn trace_panic(panic: Json<Panic>) -> Result<()> {
232    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
233    Ok(())
234}
235
236async fn get_rpc_server_snapshot(
237    Extension(rpc_server): Extension<Arc<rpc::Server>>,
238) -> Result<ErasedJson> {
239    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
240}
241
242#[derive(Deserialize)]
243struct CreateAccessTokenQueryParams {
244    public_key: String,
245    impersonate: Option<String>,
246}
247
248#[derive(Serialize)]
249struct CreateAccessTokenResponse {
250    user_id: UserId,
251    encrypted_access_token: String,
252}
253
254async fn create_access_token(
255    Path(login): Path<String>,
256    Query(params): Query<CreateAccessTokenQueryParams>,
257    Extension(app): Extension<Arc<AppState>>,
258) -> Result<Json<CreateAccessTokenResponse>> {
259    //     request.require_token().await?;
260
261    let user = app
262        .db
263        .get_user_by_github_login(&login)
264        .await?
265        .ok_or_else(|| anyhow!("user not found"))?;
266
267    let mut user_id = user.id;
268    if let Some(impersonate) = params.impersonate {
269        if user.admin {
270            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
271                user_id = impersonated_user.id;
272            } else {
273                return Err(Error::Http(
274                    StatusCode::UNPROCESSABLE_ENTITY,
275                    format!("user {impersonate} does not exist"),
276                ));
277            }
278        } else {
279            return Err(Error::Http(
280                StatusCode::UNAUTHORIZED,
281                format!("you do not have permission to impersonate other users"),
282            ));
283        }
284    }
285
286    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
287    let encrypted_access_token =
288        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
289
290    Ok(Json(CreateAccessTokenResponse {
291        user_id,
292        encrypted_access_token,
293    }))
294}
295
296async fn get_user_for_invite_code(
297    Path(code): Path<String>,
298    Extension(app): Extension<Arc<AppState>>,
299) -> Result<Json<User>> {
300    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
301}