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