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
130async fn create_user(
131 Json(params): Json<CreateUserParams>,
132 Extension(app): Extension<Arc<AppState>>,
133 Extension(rpc_server): Extension<Arc<rpc::Server>>,
134) -> Result<Json<User>> {
135 let user = NewUserParams {
136 github_login: params.github_login,
137 github_user_id: params.github_user_id,
138 invite_count: params.invite_count,
139 };
140 let (user_id, inviter_id) =
141 // Creating a user via the normal signup process
142 if let Some(email_confirmation_code) = params.email_confirmation_code {
143 app.db
144 .create_user_from_invite(
145 &Invite {
146 email_address: params.email_address,
147 email_confirmation_code,
148 },
149 user,
150 )
151 .await?
152 }
153 // Creating a user as an admin
154 else {
155 (
156 app.db
157 .create_user(¶ms.email_address, false, user)
158 .await?,
159 None,
160 )
161 };
162
163 if let Some(inviter_id) = inviter_id {
164 rpc_server
165 .invite_code_redeemed(inviter_id, user_id)
166 .await
167 .trace_err();
168 }
169
170 let user = app
171 .db
172 .get_user_by_id(user_id)
173 .await?
174 .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
175
176 Ok(Json(user))
177}
178
179#[derive(Deserialize)]
180struct UpdateUserParams {
181 admin: Option<bool>,
182 invite_count: Option<u32>,
183}
184
185async fn update_user(
186 Path(user_id): Path<i32>,
187 Json(params): Json<UpdateUserParams>,
188 Extension(app): Extension<Arc<AppState>>,
189 Extension(rpc_server): Extension<Arc<rpc::Server>>,
190) -> Result<()> {
191 let user_id = UserId(user_id);
192
193 if let Some(admin) = params.admin {
194 app.db.set_user_is_admin(user_id, admin).await?;
195 }
196
197 if let Some(invite_count) = params.invite_count {
198 app.db
199 .set_invite_count_for_user(user_id, invite_count)
200 .await?;
201 rpc_server.invite_count_updated(user_id).await.trace_err();
202 }
203
204 Ok(())
205}
206
207async fn destroy_user(
208 Path(user_id): Path<i32>,
209 Extension(app): Extension<Arc<AppState>>,
210) -> Result<()> {
211 app.db.destroy_user(UserId(user_id)).await?;
212 Ok(())
213}
214
215#[derive(Debug, Deserialize)]
216struct GetUsersWithNoInvites {
217 invited_by_another_user: bool,
218}
219
220async fn get_users_with_no_invites(
221 Query(params): Query<GetUsersWithNoInvites>,
222 Extension(app): Extension<Arc<AppState>>,
223) -> Result<Json<Vec<User>>> {
224 Ok(Json(
225 app.db
226 .get_users_with_no_invites(params.invited_by_another_user)
227 .await?,
228 ))
229}
230
231#[derive(Debug, Deserialize)]
232struct Panic {
233 version: String,
234 text: String,
235}
236
237#[instrument(skip(panic))]
238async fn trace_panic(panic: Json<Panic>) -> Result<()> {
239 tracing::error!(version = %panic.version, text = %panic.text, "panic report");
240 Ok(())
241}
242
243async fn get_rpc_server_snapshot(
244 Extension(rpc_server): Extension<Arc<rpc::Server>>,
245) -> Result<ErasedJson> {
246 Ok(ErasedJson::pretty(rpc_server.snapshot().await))
247}
248
249#[derive(Deserialize)]
250struct TimePeriodParams {
251 #[serde(with = "time::serde::iso8601")]
252 start: OffsetDateTime,
253 #[serde(with = "time::serde::iso8601")]
254 end: OffsetDateTime,
255}
256
257async fn get_top_users_activity_summary(
258 Query(params): Query<TimePeriodParams>,
259 Extension(app): Extension<Arc<AppState>>,
260) -> Result<ErasedJson> {
261 let summary = app
262 .db
263 .get_top_users_activity_summary(params.start..params.end, 100)
264 .await?;
265 Ok(ErasedJson::pretty(summary))
266}
267
268async fn get_user_activity_timeline(
269 Path(user_id): Path<i32>,
270 Query(params): Query<TimePeriodParams>,
271 Extension(app): Extension<Arc<AppState>>,
272) -> Result<ErasedJson> {
273 let summary = app
274 .db
275 .get_user_activity_timeline(params.start..params.end, UserId(user_id))
276 .await?;
277 Ok(ErasedJson::pretty(summary))
278}
279
280#[derive(Deserialize)]
281struct ActiveUserCountParams {
282 #[serde(flatten)]
283 period: TimePeriodParams,
284 durations_in_minutes: String,
285 #[serde(default)]
286 only_collaborative: bool,
287}
288
289#[derive(Serialize)]
290struct ActiveUserSet {
291 active_time_in_minutes: u64,
292 user_count: usize,
293}
294
295async fn get_active_user_counts(
296 Query(params): Query<ActiveUserCountParams>,
297 Extension(app): Extension<Arc<AppState>>,
298) -> Result<ErasedJson> {
299 let durations_in_minutes = params.durations_in_minutes.split(',');
300 let mut user_sets = Vec::new();
301 for duration in durations_in_minutes {
302 let duration = duration
303 .parse()
304 .map_err(|_| anyhow!("invalid duration: {duration}"))?;
305 user_sets.push(ActiveUserSet {
306 active_time_in_minutes: duration,
307 user_count: app
308 .db
309 .get_active_user_count(
310 params.period.start..params.period.end,
311 Duration::from_secs(duration * 60),
312 params.only_collaborative,
313 )
314 .await?,
315 })
316 }
317 Ok(ErasedJson::pretty(user_sets))
318}
319
320#[derive(Deserialize)]
321struct GetProjectMetadataParams {
322 project_id: u64,
323}
324
325async fn get_project_metadata(
326 Query(params): Query<GetProjectMetadataParams>,
327 Extension(app): Extension<Arc<AppState>>,
328) -> Result<ErasedJson> {
329 let extensions = app
330 .db
331 .get_project_extensions(ProjectId::from_proto(params.project_id))
332 .await?;
333 Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
334}
335
336#[derive(Deserialize)]
337struct CreateAccessTokenQueryParams {
338 public_key: String,
339 impersonate: Option<String>,
340}
341
342#[derive(Serialize)]
343struct CreateAccessTokenResponse {
344 user_id: UserId,
345 encrypted_access_token: String,
346}
347
348async fn create_access_token(
349 Path(user_id): Path<UserId>,
350 Query(params): Query<CreateAccessTokenQueryParams>,
351 Extension(app): Extension<Arc<AppState>>,
352) -> Result<Json<CreateAccessTokenResponse>> {
353 let user = app
354 .db
355 .get_user_by_id(user_id)
356 .await?
357 .ok_or_else(|| anyhow!("user not found"))?;
358
359 let mut user_id = user.id;
360 if let Some(impersonate) = params.impersonate {
361 if user.admin {
362 if let Some(impersonated_user) = app
363 .db
364 .get_user_by_github_account(&impersonate, None)
365 .await?
366 {
367 user_id = impersonated_user.id;
368 } else {
369 return Err(Error::Http(
370 StatusCode::UNPROCESSABLE_ENTITY,
371 format!("user {impersonate} does not exist"),
372 ));
373 }
374 } else {
375 return Err(Error::Http(
376 StatusCode::UNAUTHORIZED,
377 "you do not have permission to impersonate other users".to_string(),
378 ));
379 }
380 }
381
382 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
383 let encrypted_access_token =
384 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
385
386 Ok(Json(CreateAccessTokenResponse {
387 user_id,
388 encrypted_access_token,
389 }))
390}
391
392async fn get_user_for_invite_code(
393 Path(code): Path<String>,
394 Extension(app): Extension<Arc<AppState>>,
395) -> Result<Json<User>> {
396 Ok(Json(app.db.get_user_for_invite_code(&code).await?))
397}
398
399#[derive(Serialize)]
400struct CreateSignupResponse {
401 metrics_id: i32,
402}
403
404async fn create_signup(
405 Json(params): Json<Signup>,
406 Extension(app): Extension<Arc<AppState>>,
407) -> Result<Json<CreateSignupResponse>> {
408 let metrics_id = app.db.create_signup(params).await?;
409 Ok(Json(CreateSignupResponse { metrics_id }))
410}
411
412async fn get_waitlist_summary(
413 Extension(app): Extension<Arc<AppState>>,
414) -> Result<Json<WaitlistSummary>> {
415 Ok(Json(app.db.get_waitlist_summary().await?))
416}
417
418#[derive(Deserialize)]
419pub struct CreateInviteFromCodeParams {
420 invite_code: String,
421 email_address: String,
422}
423
424async fn create_invite_from_code(
425 Json(params): Json<CreateInviteFromCodeParams>,
426 Extension(app): Extension<Arc<AppState>>,
427) -> Result<Json<Invite>> {
428 Ok(Json(
429 app.db
430 .create_invite_from_code(¶ms.invite_code, ¶ms.email_address)
431 .await?,
432 ))
433}
434
435#[derive(Deserialize)]
436pub struct GetUnsentInvitesParams {
437 pub count: usize,
438}
439
440async fn get_unsent_invites(
441 Query(params): Query<GetUnsentInvitesParams>,
442 Extension(app): Extension<Arc<AppState>>,
443) -> Result<Json<Vec<Invite>>> {
444 Ok(Json(app.db.get_unsent_invites(params.count).await?))
445}
446
447async fn record_sent_invites(
448 Json(params): Json<Vec<Invite>>,
449 Extension(app): Extension<Arc<AppState>>,
450) -> Result<()> {
451 app.db.record_sent_invites(¶ms).await?;
452 Ok(())
453}