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