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