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