api.rs

  1use crate::{
  2    auth,
  3    db::{ProjectId, 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 serde_json::json;
 20use std::{sync::Arc, time::Duration};
 21use time::OffsetDateTime;
 22use tower::ServiceBuilder;
 23use tracing::instrument;
 24
 25pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
 26    Router::new()
 27        .route("/users", get(get_users).post(create_user))
 28        .route(
 29            "/users/:id",
 30            put(update_user).delete(destroy_user).get(get_user),
 31        )
 32        .route("/users/:id/access_tokens", post(create_access_token))
 33        .route("/bulk_users", post(create_users))
 34        .route("/users_with_no_invites", get(get_users_with_no_invites))
 35        .route("/invite_codes/:code", get(get_user_for_invite_code))
 36        .route("/panic", post(trace_panic))
 37        .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
 38        .route(
 39            "/user_activity/summary",
 40            get(get_top_users_activity_summary),
 41        )
 42        .route(
 43            "/user_activity/timeline/:user_id",
 44            get(get_user_activity_timeline),
 45        )
 46        .route("/user_activity/counts", get(get_active_user_counts))
 47        .route("/project_metadata", get(get_project_metadata))
 48        .layer(
 49            ServiceBuilder::new()
 50                .layer(Extension(state))
 51                .layer(Extension(rpc_server.clone()))
 52                .layer(middleware::from_fn(validate_api_token)),
 53        )
 54}
 55
 56pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 57    let token = req
 58        .headers()
 59        .get(http::header::AUTHORIZATION)
 60        .and_then(|header| header.to_str().ok())
 61        .ok_or_else(|| {
 62            Error::Http(
 63                StatusCode::BAD_REQUEST,
 64                "missing authorization header".to_string(),
 65            )
 66        })?
 67        .strip_prefix("token ")
 68        .ok_or_else(|| {
 69            Error::Http(
 70                StatusCode::BAD_REQUEST,
 71                "invalid authorization header".to_string(),
 72            )
 73        })?;
 74
 75    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 76
 77    if token != state.api_token {
 78        Err(Error::Http(
 79            StatusCode::UNAUTHORIZED,
 80            "invalid authorization token".to_string(),
 81        ))?
 82    }
 83
 84    Ok::<_, Error>(next.run(req).await)
 85}
 86
 87#[derive(Debug, Deserialize)]
 88struct GetUsersQueryParams {
 89    query: Option<String>,
 90    page: Option<u32>,
 91    limit: Option<u32>,
 92}
 93
 94async fn get_users(
 95    Query(params): Query<GetUsersQueryParams>,
 96    Extension(app): Extension<Arc<AppState>>,
 97) -> Result<Json<Vec<User>>> {
 98    let limit = params.limit.unwrap_or(100);
 99    let users = if let Some(query) = params.query {
100        app.db.fuzzy_search_users(&query, limit).await?
101    } else {
102        app.db
103            .get_all_users(params.page.unwrap_or(0), limit)
104            .await?
105    };
106    Ok(Json(users))
107}
108
109#[derive(Deserialize, Debug)]
110struct CreateUserParams {
111    github_login: String,
112    invite_code: Option<String>,
113    email_address: Option<String>,
114    admin: bool,
115}
116
117async fn create_user(
118    Json(params): Json<CreateUserParams>,
119    Extension(app): Extension<Arc<AppState>>,
120    Extension(rpc_server): Extension<Arc<rpc::Server>>,
121) -> Result<Json<User>> {
122    let user_id = if let Some(invite_code) = params.invite_code {
123        let invitee_id = app
124            .db
125            .redeem_invite_code(
126                &invite_code,
127                &params.github_login,
128                params.email_address.as_deref(),
129            )
130            .await?;
131        rpc_server
132            .invite_code_redeemed(&invite_code, invitee_id)
133            .await
134            .trace_err();
135        invitee_id
136    } else {
137        app.db
138            .create_user(
139                &params.github_login,
140                params.email_address.as_deref(),
141                params.admin,
142            )
143            .await?
144    };
145
146    let user = app
147        .db
148        .get_user_by_id(user_id)
149        .await?
150        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
151
152    Ok(Json(user))
153}
154
155#[derive(Deserialize)]
156struct UpdateUserParams {
157    admin: Option<bool>,
158    invite_count: Option<u32>,
159}
160
161async fn update_user(
162    Path(user_id): Path<i32>,
163    Json(params): Json<UpdateUserParams>,
164    Extension(app): Extension<Arc<AppState>>,
165    Extension(rpc_server): Extension<Arc<rpc::Server>>,
166) -> Result<()> {
167    let user_id = UserId(user_id);
168
169    if let Some(admin) = params.admin {
170        app.db.set_user_is_admin(user_id, admin).await?;
171    }
172
173    if let Some(invite_count) = params.invite_count {
174        app.db.set_invite_count(user_id, invite_count).await?;
175        rpc_server.invite_count_updated(user_id).await.trace_err();
176    }
177
178    Ok(())
179}
180
181async fn destroy_user(
182    Path(user_id): Path<i32>,
183    Extension(app): Extension<Arc<AppState>>,
184) -> Result<()> {
185    app.db.destroy_user(UserId(user_id)).await?;
186    Ok(())
187}
188
189async fn get_user(
190    Path(login): Path<String>,
191    Extension(app): Extension<Arc<AppState>>,
192) -> Result<Json<User>> {
193    let user = app
194        .db
195        .get_user_by_github_login(&login)
196        .await?
197        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
198    Ok(Json(user))
199}
200
201#[derive(Deserialize)]
202struct CreateUsersParams {
203    users: Vec<CreateUsersEntry>,
204}
205
206#[derive(Deserialize)]
207struct CreateUsersEntry {
208    github_login: String,
209    email_address: String,
210    invite_count: usize,
211}
212
213async fn create_users(
214    Json(params): Json<CreateUsersParams>,
215    Extension(app): Extension<Arc<AppState>>,
216) -> Result<Json<Vec<User>>> {
217    let user_ids = app
218        .db
219        .create_users(
220            params
221                .users
222                .into_iter()
223                .map(|params| {
224                    (
225                        params.github_login,
226                        params.email_address,
227                        params.invite_count,
228                    )
229                })
230                .collect(),
231        )
232        .await?;
233    let users = app.db.get_users_by_ids(user_ids).await?;
234    Ok(Json(users))
235}
236
237#[derive(Debug, Deserialize)]
238struct GetUsersWithNoInvites {
239    invited_by_another_user: bool,
240}
241
242async fn get_users_with_no_invites(
243    Query(params): Query<GetUsersWithNoInvites>,
244    Extension(app): Extension<Arc<AppState>>,
245) -> Result<Json<Vec<User>>> {
246    Ok(Json(
247        app.db
248            .get_users_with_no_invites(params.invited_by_another_user)
249            .await?,
250    ))
251}
252
253#[derive(Debug, Deserialize)]
254struct Panic {
255    version: String,
256    text: String,
257}
258
259#[instrument(skip(panic))]
260async fn trace_panic(panic: Json<Panic>) -> Result<()> {
261    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
262    Ok(())
263}
264
265async fn get_rpc_server_snapshot(
266    Extension(rpc_server): Extension<Arc<rpc::Server>>,
267) -> Result<ErasedJson> {
268    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
269}
270
271#[derive(Deserialize)]
272struct TimePeriodParams {
273    #[serde(with = "time::serde::iso8601")]
274    start: OffsetDateTime,
275    #[serde(with = "time::serde::iso8601")]
276    end: OffsetDateTime,
277}
278
279async fn get_top_users_activity_summary(
280    Query(params): Query<TimePeriodParams>,
281    Extension(app): Extension<Arc<AppState>>,
282) -> Result<ErasedJson> {
283    let summary = app
284        .db
285        .get_top_users_activity_summary(params.start..params.end, 100)
286        .await?;
287    Ok(ErasedJson::pretty(summary))
288}
289
290async fn get_user_activity_timeline(
291    Path(user_id): Path<i32>,
292    Query(params): Query<TimePeriodParams>,
293    Extension(app): Extension<Arc<AppState>>,
294) -> Result<ErasedJson> {
295    let summary = app
296        .db
297        .get_user_activity_timeline(params.start..params.end, UserId(user_id))
298        .await?;
299    Ok(ErasedJson::pretty(summary))
300}
301
302#[derive(Deserialize)]
303struct ActiveUserCountParams {
304    #[serde(flatten)]
305    period: TimePeriodParams,
306    durations_in_minutes: String,
307    #[serde(default)]
308    only_collaborative: bool,
309}
310
311#[derive(Serialize)]
312struct ActiveUserSet {
313    active_time_in_minutes: u64,
314    user_count: usize,
315}
316
317async fn get_active_user_counts(
318    Query(params): Query<ActiveUserCountParams>,
319    Extension(app): Extension<Arc<AppState>>,
320) -> Result<ErasedJson> {
321    let durations_in_minutes = params.durations_in_minutes.split(',');
322    let mut user_sets = Vec::new();
323    for duration in durations_in_minutes {
324        let duration = duration
325            .parse()
326            .map_err(|_| anyhow!("invalid duration: {duration}"))?;
327        user_sets.push(ActiveUserSet {
328            active_time_in_minutes: duration,
329            user_count: app
330                .db
331                .get_active_user_count(
332                    params.period.start..params.period.end,
333                    Duration::from_secs(duration * 60),
334                    params.only_collaborative,
335                )
336                .await?,
337        })
338    }
339    Ok(ErasedJson::pretty(user_sets))
340}
341
342#[derive(Deserialize)]
343struct GetProjectMetadataParams {
344    project_id: u64,
345}
346
347async fn get_project_metadata(
348    Query(params): Query<GetProjectMetadataParams>,
349    Extension(app): Extension<Arc<AppState>>,
350) -> Result<ErasedJson> {
351    let extensions = app
352        .db
353        .get_project_extensions(ProjectId::from_proto(params.project_id))
354        .await?;
355    Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
356}
357
358#[derive(Deserialize)]
359struct CreateAccessTokenQueryParams {
360    public_key: String,
361    impersonate: Option<String>,
362}
363
364#[derive(Serialize)]
365struct CreateAccessTokenResponse {
366    user_id: UserId,
367    encrypted_access_token: String,
368}
369
370async fn create_access_token(
371    Path(login): Path<String>,
372    Query(params): Query<CreateAccessTokenQueryParams>,
373    Extension(app): Extension<Arc<AppState>>,
374) -> Result<Json<CreateAccessTokenResponse>> {
375    //     request.require_token().await?;
376
377    let user = app
378        .db
379        .get_user_by_github_login(&login)
380        .await?
381        .ok_or_else(|| anyhow!("user not found"))?;
382
383    let mut user_id = user.id;
384    if let Some(impersonate) = params.impersonate {
385        if user.admin {
386            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
387                user_id = impersonated_user.id;
388            } else {
389                return Err(Error::Http(
390                    StatusCode::UNPROCESSABLE_ENTITY,
391                    format!("user {impersonate} does not exist"),
392                ));
393            }
394        } else {
395            return Err(Error::Http(
396                StatusCode::UNAUTHORIZED,
397                "you do not have permission to impersonate other users".to_string(),
398            ));
399        }
400    }
401
402    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
403    let encrypted_access_token =
404        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
405
406    Ok(Json(CreateAccessTokenResponse {
407        user_id,
408        encrypted_access_token,
409    }))
410}
411
412async fn get_user_for_invite_code(
413    Path(code): Path<String>,
414    Extension(app): Extension<Arc<AppState>>,
415) -> Result<Json<User>> {
416    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
417}