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