api.rs

  1use crate::{
  2    auth,
  3    db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
  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("/user", get(get_authenticated_user))
 28        .route("/users", get(get_users).post(create_user))
 29        .route("/users/:id", put(update_user).delete(destroy_user))
 30        .route("/users/:id/access_tokens", post(create_access_token))
 31        .route("/users_with_no_invites", get(get_users_with_no_invites))
 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        .route(
 36            "/user_activity/summary",
 37            get(get_top_users_activity_summary),
 38        )
 39        .route(
 40            "/user_activity/timeline/:user_id",
 41            get(get_user_activity_timeline),
 42        )
 43        .route("/user_activity/counts", get(get_active_user_counts))
 44        .route("/project_metadata", get(get_project_metadata))
 45        .route("/signups", post(create_signup))
 46        .route("/signups_summary", get(get_waitlist_summary))
 47        .route("/user_invites", post(create_invite_from_code))
 48        .route("/unsent_invites", get(get_unsent_invites))
 49        .route("/sent_invites", post(record_sent_invites))
 50        .layer(
 51            ServiceBuilder::new()
 52                .layer(Extension(state))
 53                .layer(Extension(rpc_server))
 54                .layer(middleware::from_fn(validate_api_token)),
 55        )
 56}
 57
 58pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 59    let token = req
 60        .headers()
 61        .get(http::header::AUTHORIZATION)
 62        .and_then(|header| header.to_str().ok())
 63        .ok_or_else(|| {
 64            Error::Http(
 65                StatusCode::BAD_REQUEST,
 66                "missing authorization header".to_string(),
 67            )
 68        })?
 69        .strip_prefix("token ")
 70        .ok_or_else(|| {
 71            Error::Http(
 72                StatusCode::BAD_REQUEST,
 73                "invalid authorization header".to_string(),
 74            )
 75        })?;
 76
 77    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 78
 79    if token != state.config.api_token {
 80        Err(Error::Http(
 81            StatusCode::UNAUTHORIZED,
 82            "invalid authorization token".to_string(),
 83        ))?
 84    }
 85
 86    Ok::<_, Error>(next.run(req).await)
 87}
 88
 89#[derive(Debug, Deserialize)]
 90struct AuthenticatedUserParams {
 91    github_user_id: Option<i32>,
 92    github_login: String,
 93}
 94
 95#[derive(Debug, Serialize)]
 96struct AuthenticatedUserResponse {
 97    user: User,
 98    metrics_id: String,
 99}
100
101async fn get_authenticated_user(
102    Query(params): Query<AuthenticatedUserParams>,
103    Extension(app): Extension<Arc<AppState>>,
104) -> Result<Json<AuthenticatedUserResponse>> {
105    let user = app
106        .db
107        .get_user_by_github_account(&params.github_login, params.github_user_id)
108        .await?
109        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
110    let metrics_id = app.db.get_user_metrics_id(user.id).await?;
111    return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
112}
113
114#[derive(Debug, Deserialize)]
115struct GetUsersQueryParams {
116    query: Option<String>,
117    page: Option<u32>,
118    limit: Option<u32>,
119}
120
121async fn get_users(
122    Query(params): Query<GetUsersQueryParams>,
123    Extension(app): Extension<Arc<AppState>>,
124) -> Result<Json<Vec<User>>> {
125    let limit = params.limit.unwrap_or(100);
126    let users = if let Some(query) = params.query {
127        app.db.fuzzy_search_users(&query, limit).await?
128    } else {
129        app.db
130            .get_all_users(params.page.unwrap_or(0), limit)
131            .await?
132    };
133    Ok(Json(users))
134}
135
136#[derive(Deserialize, Debug)]
137struct CreateUserParams {
138    github_user_id: i32,
139    github_login: String,
140    email_address: String,
141    email_confirmation_code: Option<String>,
142    #[serde(default)]
143    admin: bool,
144    #[serde(default)]
145    invite_count: i32,
146}
147
148#[derive(Serialize, Debug)]
149struct CreateUserResponse {
150    user: User,
151    signup_device_id: Option<String>,
152    metrics_id: String,
153}
154
155async fn create_user(
156    Json(params): Json<CreateUserParams>,
157    Extension(app): Extension<Arc<AppState>>,
158    Extension(rpc_server): Extension<Arc<rpc::Server>>,
159) -> Result<Json<Option<CreateUserResponse>>> {
160    let user = NewUserParams {
161        github_login: params.github_login,
162        github_user_id: params.github_user_id,
163        invite_count: params.invite_count,
164    };
165
166    // Creating a user via the normal signup process
167    let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
168        if let Some(result) = app
169            .db
170            .create_user_from_invite(
171                &Invite {
172                    email_address: params.email_address,
173                    email_confirmation_code,
174                },
175                user,
176            )
177            .await?
178        {
179            result
180        } else {
181            return Ok(Json(None));
182        }
183    }
184    // Creating a user as an admin
185    else if params.admin {
186        app.db
187            .create_user(&params.email_address, false, user)
188            .await?
189    } else {
190        Err(Error::Http(
191            StatusCode::UNPROCESSABLE_ENTITY,
192            "email confirmation code is required".into(),
193        ))?
194    };
195
196    if let Some(inviter_id) = result.inviting_user_id {
197        rpc_server
198            .invite_code_redeemed(inviter_id, result.user_id)
199            .await
200            .trace_err();
201    }
202
203    let user = app
204        .db
205        .get_user_by_id(result.user_id)
206        .await?
207        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
208
209    Ok(Json(Some(CreateUserResponse {
210        user,
211        metrics_id: result.metrics_id,
212        signup_device_id: result.signup_device_id,
213    })))
214}
215
216#[derive(Deserialize)]
217struct UpdateUserParams {
218    admin: Option<bool>,
219    invite_count: Option<u32>,
220}
221
222async fn update_user(
223    Path(user_id): Path<i32>,
224    Json(params): Json<UpdateUserParams>,
225    Extension(app): Extension<Arc<AppState>>,
226    Extension(rpc_server): Extension<Arc<rpc::Server>>,
227) -> Result<()> {
228    let user_id = UserId(user_id);
229
230    if let Some(admin) = params.admin {
231        app.db.set_user_is_admin(user_id, admin).await?;
232    }
233
234    if let Some(invite_count) = params.invite_count {
235        app.db
236            .set_invite_count_for_user(user_id, invite_count)
237            .await?;
238        rpc_server.invite_count_updated(user_id).await.trace_err();
239    }
240
241    Ok(())
242}
243
244async fn destroy_user(
245    Path(user_id): Path<i32>,
246    Extension(app): Extension<Arc<AppState>>,
247) -> Result<()> {
248    app.db.destroy_user(UserId(user_id)).await?;
249    Ok(())
250}
251
252#[derive(Debug, Deserialize)]
253struct GetUsersWithNoInvites {
254    invited_by_another_user: bool,
255}
256
257async fn get_users_with_no_invites(
258    Query(params): Query<GetUsersWithNoInvites>,
259    Extension(app): Extension<Arc<AppState>>,
260) -> Result<Json<Vec<User>>> {
261    Ok(Json(
262        app.db
263            .get_users_with_no_invites(params.invited_by_another_user)
264            .await?,
265    ))
266}
267
268#[derive(Debug, Deserialize)]
269struct Panic {
270    version: String,
271    text: String,
272}
273
274#[instrument(skip(panic))]
275async fn trace_panic(panic: Json<Panic>) -> Result<()> {
276    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
277    Ok(())
278}
279
280async fn get_rpc_server_snapshot(
281    Extension(rpc_server): Extension<Arc<rpc::Server>>,
282) -> Result<ErasedJson> {
283    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
284}
285
286#[derive(Deserialize)]
287struct TimePeriodParams {
288    #[serde(with = "time::serde::iso8601")]
289    start: OffsetDateTime,
290    #[serde(with = "time::serde::iso8601")]
291    end: OffsetDateTime,
292}
293
294async fn get_top_users_activity_summary(
295    Query(params): Query<TimePeriodParams>,
296    Extension(app): Extension<Arc<AppState>>,
297) -> Result<ErasedJson> {
298    let summary = app
299        .db
300        .get_top_users_activity_summary(params.start..params.end, 100)
301        .await?;
302    Ok(ErasedJson::pretty(summary))
303}
304
305async fn get_user_activity_timeline(
306    Path(user_id): Path<i32>,
307    Query(params): Query<TimePeriodParams>,
308    Extension(app): Extension<Arc<AppState>>,
309) -> Result<ErasedJson> {
310    let summary = app
311        .db
312        .get_user_activity_timeline(params.start..params.end, UserId(user_id))
313        .await?;
314    Ok(ErasedJson::pretty(summary))
315}
316
317#[derive(Deserialize)]
318struct ActiveUserCountParams {
319    #[serde(flatten)]
320    period: TimePeriodParams,
321    durations_in_minutes: String,
322    #[serde(default)]
323    only_collaborative: bool,
324}
325
326#[derive(Serialize)]
327struct ActiveUserSet {
328    active_time_in_minutes: u64,
329    user_count: usize,
330}
331
332async fn get_active_user_counts(
333    Query(params): Query<ActiveUserCountParams>,
334    Extension(app): Extension<Arc<AppState>>,
335) -> Result<ErasedJson> {
336    let durations_in_minutes = params.durations_in_minutes.split(',');
337    let mut user_sets = Vec::new();
338    for duration in durations_in_minutes {
339        let duration = duration
340            .parse()
341            .map_err(|_| anyhow!("invalid duration: {duration}"))?;
342        user_sets.push(ActiveUserSet {
343            active_time_in_minutes: duration,
344            user_count: app
345                .db
346                .get_active_user_count(
347                    params.period.start..params.period.end,
348                    Duration::from_secs(duration * 60),
349                    params.only_collaborative,
350                )
351                .await?,
352        })
353    }
354    Ok(ErasedJson::pretty(user_sets))
355}
356
357#[derive(Deserialize)]
358struct GetProjectMetadataParams {
359    project_id: u64,
360}
361
362async fn get_project_metadata(
363    Query(params): Query<GetProjectMetadataParams>,
364    Extension(app): Extension<Arc<AppState>>,
365) -> Result<ErasedJson> {
366    let extensions = app
367        .db
368        .get_project_extensions(ProjectId::from_proto(params.project_id))
369        .await?;
370    Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
371}
372
373#[derive(Deserialize)]
374struct CreateAccessTokenQueryParams {
375    public_key: String,
376    impersonate: Option<String>,
377}
378
379#[derive(Serialize)]
380struct CreateAccessTokenResponse {
381    user_id: UserId,
382    encrypted_access_token: String,
383}
384
385async fn create_access_token(
386    Path(user_id): Path<UserId>,
387    Query(params): Query<CreateAccessTokenQueryParams>,
388    Extension(app): Extension<Arc<AppState>>,
389) -> Result<Json<CreateAccessTokenResponse>> {
390    let user = app
391        .db
392        .get_user_by_id(user_id)
393        .await?
394        .ok_or_else(|| anyhow!("user not found"))?;
395
396    let mut user_id = user.id;
397    if let Some(impersonate) = params.impersonate {
398        if user.admin {
399            if let Some(impersonated_user) = app
400                .db
401                .get_user_by_github_account(&impersonate, None)
402                .await?
403            {
404                user_id = impersonated_user.id;
405            } else {
406                return Err(Error::Http(
407                    StatusCode::UNPROCESSABLE_ENTITY,
408                    format!("user {impersonate} does not exist"),
409                ));
410            }
411        } else {
412            return Err(Error::Http(
413                StatusCode::UNAUTHORIZED,
414                "you do not have permission to impersonate other users".to_string(),
415            ));
416        }
417    }
418
419    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
420    let encrypted_access_token =
421        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
422
423    Ok(Json(CreateAccessTokenResponse {
424        user_id,
425        encrypted_access_token,
426    }))
427}
428
429async fn get_user_for_invite_code(
430    Path(code): Path<String>,
431    Extension(app): Extension<Arc<AppState>>,
432) -> Result<Json<User>> {
433    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
434}
435
436async fn create_signup(
437    Json(params): Json<Signup>,
438    Extension(app): Extension<Arc<AppState>>,
439) -> Result<()> {
440    app.db.create_signup(params).await?;
441    Ok(())
442}
443
444async fn get_waitlist_summary(
445    Extension(app): Extension<Arc<AppState>>,
446) -> Result<Json<WaitlistSummary>> {
447    Ok(Json(app.db.get_waitlist_summary().await?))
448}
449
450#[derive(Deserialize)]
451pub struct CreateInviteFromCodeParams {
452    invite_code: String,
453    email_address: String,
454    device_id: Option<String>,
455}
456
457async fn create_invite_from_code(
458    Json(params): Json<CreateInviteFromCodeParams>,
459    Extension(app): Extension<Arc<AppState>>,
460) -> Result<Json<Invite>> {
461    Ok(Json(
462        app.db
463            .create_invite_from_code(
464                &params.invite_code,
465                &params.email_address,
466                params.device_id.as_deref(),
467            )
468            .await?,
469    ))
470}
471
472#[derive(Deserialize)]
473pub struct GetUnsentInvitesParams {
474    pub count: usize,
475}
476
477async fn get_unsent_invites(
478    Query(params): Query<GetUnsentInvitesParams>,
479    Extension(app): Extension<Arc<AppState>>,
480) -> Result<Json<Vec<Invite>>> {
481    Ok(Json(app.db.get_unsent_invites(params.count).await?))
482}
483
484async fn record_sent_invites(
485    Json(params): Json<Vec<Invite>>,
486    Extension(app): Extension<Arc<AppState>>,
487) -> Result<()> {
488    app.db.record_sent_invites(&params).await?;
489    Ok(())
490}