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