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}
 82
 83#[derive(Debug, Serialize)]
 84struct AuthenticatedUserResponse {
 85    user: User,
 86    metrics_id: String,
 87}
 88
 89async fn get_authenticated_user(
 90    Query(params): Query<AuthenticatedUserParams>,
 91    Extension(app): Extension<Arc<AppState>>,
 92) -> Result<Json<AuthenticatedUserResponse>> {
 93    let user = app
 94        .db
 95        .get_user_by_github_account(&params.github_login, params.github_user_id)
 96        .await?
 97        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
 98    let metrics_id = app.db.get_user_metrics_id(user.id).await?;
 99    return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
100}
101
102#[derive(Debug, Deserialize)]
103struct GetUsersQueryParams {
104    query: Option<String>,
105    page: Option<u32>,
106    limit: Option<u32>,
107}
108
109async fn get_users(
110    Query(params): Query<GetUsersQueryParams>,
111    Extension(app): Extension<Arc<AppState>>,
112) -> Result<Json<Vec<User>>> {
113    let limit = params.limit.unwrap_or(100);
114    let users = if let Some(query) = params.query {
115        app.db.fuzzy_search_users(&query, limit).await?
116    } else {
117        app.db
118            .get_all_users(params.page.unwrap_or(0), limit)
119            .await?
120    };
121    Ok(Json(users))
122}
123
124#[derive(Deserialize, Debug)]
125struct CreateUserParams {
126    github_user_id: i32,
127    github_login: String,
128    email_address: String,
129    email_confirmation_code: Option<String>,
130    #[serde(default)]
131    admin: bool,
132    #[serde(default)]
133    invite_count: i32,
134}
135
136#[derive(Serialize, Debug)]
137struct CreateUserResponse {
138    user: User,
139    signup_device_id: Option<String>,
140    metrics_id: String,
141}
142
143async fn create_user(
144    Json(params): Json<CreateUserParams>,
145    Extension(app): Extension<Arc<AppState>>,
146    Extension(rpc_server): Extension<Arc<rpc::Server>>,
147) -> Result<Json<Option<CreateUserResponse>>> {
148    let user = NewUserParams {
149        github_login: params.github_login,
150        github_user_id: params.github_user_id,
151        invite_count: params.invite_count,
152    };
153
154    // Creating a user via the normal signup process
155    let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
156        if let Some(result) = app
157            .db
158            .create_user_from_invite(
159                &Invite {
160                    email_address: params.email_address,
161                    email_confirmation_code,
162                },
163                user,
164            )
165            .await?
166        {
167            result
168        } else {
169            return Ok(Json(None));
170        }
171    }
172    // Creating a user as an admin
173    else if params.admin {
174        app.db
175            .create_user(&params.email_address, false, user)
176            .await?
177    } else {
178        Err(Error::Http(
179            StatusCode::UNPROCESSABLE_ENTITY,
180            "email confirmation code is required".into(),
181        ))?
182    };
183
184    if let Some(inviter_id) = result.inviting_user_id {
185        rpc_server
186            .invite_code_redeemed(inviter_id, result.user_id)
187            .await
188            .trace_err();
189    }
190
191    let user = app
192        .db
193        .get_user_by_id(result.user_id)
194        .await?
195        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
196
197    Ok(Json(Some(CreateUserResponse {
198        user,
199        metrics_id: result.metrics_id,
200        signup_device_id: result.signup_device_id,
201    })))
202}
203
204#[derive(Deserialize)]
205struct UpdateUserParams {
206    admin: Option<bool>,
207    invite_count: Option<i32>,
208}
209
210async fn update_user(
211    Path(user_id): Path<i32>,
212    Json(params): Json<UpdateUserParams>,
213    Extension(app): Extension<Arc<AppState>>,
214    Extension(rpc_server): Extension<Arc<rpc::Server>>,
215) -> Result<()> {
216    let user_id = UserId(user_id);
217
218    if let Some(admin) = params.admin {
219        app.db.set_user_is_admin(user_id, admin).await?;
220    }
221
222    if let Some(invite_count) = params.invite_count {
223        app.db
224            .set_invite_count_for_user(user_id, invite_count)
225            .await?;
226        rpc_server.invite_count_updated(user_id).await.trace_err();
227    }
228
229    Ok(())
230}
231
232async fn destroy_user(
233    Path(user_id): Path<i32>,
234    Extension(app): Extension<Arc<AppState>>,
235) -> Result<()> {
236    app.db.destroy_user(UserId(user_id)).await?;
237    Ok(())
238}
239
240#[derive(Debug, Deserialize)]
241struct GetUsersWithNoInvites {
242    invited_by_another_user: bool,
243}
244
245async fn get_users_with_no_invites(
246    Query(params): Query<GetUsersWithNoInvites>,
247    Extension(app): Extension<Arc<AppState>>,
248) -> Result<Json<Vec<User>>> {
249    Ok(Json(
250        app.db
251            .get_users_with_no_invites(params.invited_by_another_user)
252            .await?,
253    ))
254}
255
256#[derive(Debug, Deserialize)]
257struct Panic {
258    version: String,
259    text: String,
260}
261
262#[instrument(skip(panic))]
263async fn trace_panic(panic: Json<Panic>) -> Result<()> {
264    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
265    Ok(())
266}
267
268async fn get_rpc_server_snapshot(
269    Extension(rpc_server): Extension<Arc<rpc::Server>>,
270) -> Result<ErasedJson> {
271    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
272}
273
274#[derive(Deserialize)]
275struct CreateAccessTokenQueryParams {
276    public_key: String,
277    impersonate: Option<String>,
278}
279
280#[derive(Serialize)]
281struct CreateAccessTokenResponse {
282    user_id: UserId,
283    encrypted_access_token: String,
284}
285
286async fn create_access_token(
287    Path(user_id): Path<UserId>,
288    Query(params): Query<CreateAccessTokenQueryParams>,
289    Extension(app): Extension<Arc<AppState>>,
290) -> Result<Json<CreateAccessTokenResponse>> {
291    let user = app
292        .db
293        .get_user_by_id(user_id)
294        .await?
295        .ok_or_else(|| anyhow!("user not found"))?;
296
297    let mut user_id = user.id;
298    if let Some(impersonate) = params.impersonate {
299        if user.admin {
300            if let Some(impersonated_user) = app
301                .db
302                .get_user_by_github_account(&impersonate, None)
303                .await?
304            {
305                user_id = impersonated_user.id;
306            } else {
307                return Err(Error::Http(
308                    StatusCode::UNPROCESSABLE_ENTITY,
309                    format!("user {impersonate} does not exist"),
310                ));
311            }
312        } else {
313            return Err(Error::Http(
314                StatusCode::UNAUTHORIZED,
315                "you do not have permission to impersonate other users".to_string(),
316            ));
317        }
318    }
319
320    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
321    let encrypted_access_token =
322        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
323
324    Ok(Json(CreateAccessTokenResponse {
325        user_id,
326        encrypted_access_token,
327    }))
328}
329
330async fn get_user_for_invite_code(
331    Path(code): Path<String>,
332    Extension(app): Extension<Arc<AppState>>,
333) -> Result<Json<User>> {
334    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
335}
336
337async fn create_signup(
338    Json(params): Json<NewSignup>,
339    Extension(app): Extension<Arc<AppState>>,
340) -> Result<()> {
341    app.db.create_signup(&params).await?;
342    Ok(())
343}
344
345async fn get_waitlist_summary(
346    Extension(app): Extension<Arc<AppState>>,
347) -> Result<Json<WaitlistSummary>> {
348    Ok(Json(app.db.get_waitlist_summary().await?))
349}
350
351#[derive(Deserialize)]
352pub struct CreateInviteFromCodeParams {
353    invite_code: String,
354    email_address: String,
355    device_id: Option<String>,
356}
357
358async fn create_invite_from_code(
359    Json(params): Json<CreateInviteFromCodeParams>,
360    Extension(app): Extension<Arc<AppState>>,
361) -> Result<Json<Invite>> {
362    Ok(Json(
363        app.db
364            .create_invite_from_code(
365                &params.invite_code,
366                &params.email_address,
367                params.device_id.as_deref(),
368            )
369            .await?,
370    ))
371}
372
373#[derive(Deserialize)]
374pub struct GetUnsentInvitesParams {
375    pub count: usize,
376}
377
378async fn get_unsent_invites(
379    Query(params): Query<GetUnsentInvitesParams>,
380    Extension(app): Extension<Arc<AppState>>,
381) -> Result<Json<Vec<Invite>>> {
382    Ok(Json(app.db.get_unsent_invites(params.count).await?))
383}
384
385async fn record_sent_invites(
386    Json(params): Json<Vec<Invite>>,
387    Extension(app): Extension<Arc<AppState>>,
388) -> Result<()> {
389    app.db.record_sent_invites(&params).await?;
390    Ok(())
391}