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