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