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