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 ¶ms.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(¶ms.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(¶ms).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 ¶ms.invite_code,
369 ¶ms.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(¶ms).await?;
394 Ok(())
395}