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