api.rs

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