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(¶ms.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(¶ms.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(¶ms).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 ¶ms.invite_code,
366 ¶ms.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(¶ms).await?;
390 Ok(())
391}