1use crate::{
2 auth,
3 db::{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 std::{sync::Arc, time::Duration};
20use time::OffsetDateTime;
21use tower::ServiceBuilder;
22use tracing::instrument;
23
24pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
25 Router::new()
26 .route("/users", get(get_users).post(create_user))
27 .route(
28 "/users/:id",
29 put(update_user).delete(destroy_user).get(get_user),
30 )
31 .route("/users/:id/access_tokens", post(create_access_token))
32 .route("/bulk_users", post(create_users))
33 .route("/invite_codes/:code", get(get_user_for_invite_code))
34 .route("/panic", post(trace_panic))
35 .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
36 .route(
37 "/project_activity_summary",
38 get(get_project_activity_summary),
39 )
40 .layer(
41 ServiceBuilder::new()
42 .layer(Extension(state))
43 .layer(Extension(rpc_server.clone()))
44 .layer(middleware::from_fn(validate_api_token)),
45 )
46}
47
48pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
49 let token = req
50 .headers()
51 .get(http::header::AUTHORIZATION)
52 .and_then(|header| header.to_str().ok())
53 .ok_or_else(|| {
54 Error::Http(
55 StatusCode::BAD_REQUEST,
56 "missing authorization header".to_string(),
57 )
58 })?
59 .strip_prefix("token ")
60 .ok_or_else(|| {
61 Error::Http(
62 StatusCode::BAD_REQUEST,
63 "invalid authorization header".to_string(),
64 )
65 })?;
66
67 let state = req.extensions().get::<Arc<AppState>>().unwrap();
68
69 if token != state.api_token {
70 Err(Error::Http(
71 StatusCode::UNAUTHORIZED,
72 "invalid authorization token".to_string(),
73 ))?
74 }
75
76 Ok::<_, Error>(next.run(req).await)
77}
78
79#[derive(Debug, Deserialize)]
80struct GetUsersQueryParams {
81 query: Option<String>,
82 page: Option<u32>,
83 limit: Option<u32>,
84}
85
86async fn get_users(
87 Query(params): Query<GetUsersQueryParams>,
88 Extension(app): Extension<Arc<AppState>>,
89) -> Result<Json<Vec<User>>> {
90 let limit = params.limit.unwrap_or(100);
91 let users = if let Some(query) = params.query {
92 app.db.fuzzy_search_users(&query, limit).await?
93 } else {
94 app.db
95 .get_all_users(params.page.unwrap_or(0), limit)
96 .await?
97 };
98 Ok(Json(users))
99}
100
101#[derive(Deserialize, Debug)]
102struct CreateUserParams {
103 github_login: String,
104 invite_code: Option<String>,
105 email_address: Option<String>,
106 admin: bool,
107}
108
109async fn create_user(
110 Json(params): Json<CreateUserParams>,
111 Extension(app): Extension<Arc<AppState>>,
112 Extension(rpc_server): Extension<Arc<rpc::Server>>,
113) -> Result<Json<User>> {
114 let user_id = if let Some(invite_code) = params.invite_code {
115 let invitee_id = app
116 .db
117 .redeem_invite_code(
118 &invite_code,
119 ¶ms.github_login,
120 params.email_address.as_deref(),
121 )
122 .await?;
123 rpc_server
124 .invite_code_redeemed(&invite_code, invitee_id)
125 .await
126 .trace_err();
127 invitee_id
128 } else {
129 app.db
130 .create_user(
131 ¶ms.github_login,
132 params.email_address.as_deref(),
133 params.admin,
134 )
135 .await?
136 };
137
138 let user = app
139 .db
140 .get_user_by_id(user_id)
141 .await?
142 .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
143
144 Ok(Json(user))
145}
146
147#[derive(Deserialize)]
148struct UpdateUserParams {
149 admin: Option<bool>,
150 invite_count: Option<u32>,
151}
152
153async fn update_user(
154 Path(user_id): Path<i32>,
155 Json(params): Json<UpdateUserParams>,
156 Extension(app): Extension<Arc<AppState>>,
157 Extension(rpc_server): Extension<Arc<rpc::Server>>,
158) -> Result<()> {
159 let user_id = UserId(user_id);
160
161 if let Some(admin) = params.admin {
162 app.db.set_user_is_admin(user_id, admin).await?;
163 }
164
165 if let Some(invite_count) = params.invite_count {
166 app.db.set_invite_count(user_id, invite_count).await?;
167 rpc_server.invite_count_updated(user_id).await.trace_err();
168 }
169
170 Ok(())
171}
172
173async fn destroy_user(
174 Path(user_id): Path<i32>,
175 Extension(app): Extension<Arc<AppState>>,
176) -> Result<()> {
177 app.db.destroy_user(UserId(user_id)).await?;
178 Ok(())
179}
180
181async fn get_user(
182 Path(login): Path<String>,
183 Extension(app): Extension<Arc<AppState>>,
184) -> Result<Json<User>> {
185 let user = app
186 .db
187 .get_user_by_github_login(&login)
188 .await?
189 .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
190 Ok(Json(user))
191}
192
193#[derive(Deserialize)]
194struct CreateUsersParams {
195 users: Vec<CreateUsersEntry>,
196}
197
198#[derive(Deserialize)]
199struct CreateUsersEntry {
200 github_login: String,
201 email_address: String,
202 invite_count: usize,
203}
204
205async fn create_users(
206 Json(params): Json<CreateUsersParams>,
207 Extension(app): Extension<Arc<AppState>>,
208) -> Result<Json<Vec<User>>> {
209 let user_ids = app
210 .db
211 .create_users(
212 params
213 .users
214 .into_iter()
215 .map(|params| {
216 (
217 params.github_login,
218 params.email_address,
219 params.invite_count,
220 )
221 })
222 .collect(),
223 )
224 .await?;
225 let users = app.db.get_users_by_ids(user_ids).await?;
226 Ok(Json(users))
227}
228
229#[derive(Debug, Deserialize)]
230struct Panic {
231 version: String,
232 text: String,
233}
234
235#[instrument(skip(panic))]
236async fn trace_panic(panic: Json<Panic>) -> Result<()> {
237 tracing::error!(version = %panic.version, text = %panic.text, "panic report");
238 Ok(())
239}
240
241async fn get_rpc_server_snapshot(
242 Extension(rpc_server): Extension<Arc<rpc::Server>>,
243) -> Result<ErasedJson> {
244 Ok(ErasedJson::pretty(rpc_server.snapshot().await))
245}
246
247#[derive(Deserialize)]
248struct GetProjectActivityParams {
249 duration_secs: u64,
250}
251
252async fn get_project_activity_summary(
253 Query(params): Query<GetProjectActivityParams>,
254 Extension(app): Extension<Arc<AppState>>,
255) -> Result<ErasedJson> {
256 let end = OffsetDateTime::now_utc();
257 let start = end - Duration::from_secs(params.duration_secs);
258 let summary = app.db.summarize_project_activity(start..end, 100).await?;
259 Ok(ErasedJson::pretty(summary))
260}
261
262#[derive(Deserialize)]
263struct CreateAccessTokenQueryParams {
264 public_key: String,
265 impersonate: Option<String>,
266}
267
268#[derive(Serialize)]
269struct CreateAccessTokenResponse {
270 user_id: UserId,
271 encrypted_access_token: String,
272}
273
274async fn create_access_token(
275 Path(login): Path<String>,
276 Query(params): Query<CreateAccessTokenQueryParams>,
277 Extension(app): Extension<Arc<AppState>>,
278) -> Result<Json<CreateAccessTokenResponse>> {
279 // request.require_token().await?;
280
281 let user = app
282 .db
283 .get_user_by_github_login(&login)
284 .await?
285 .ok_or_else(|| anyhow!("user not found"))?;
286
287 let mut user_id = user.id;
288 if let Some(impersonate) = params.impersonate {
289 if user.admin {
290 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
291 user_id = impersonated_user.id;
292 } else {
293 return Err(Error::Http(
294 StatusCode::UNPROCESSABLE_ENTITY,
295 format!("user {impersonate} does not exist"),
296 ));
297 }
298 } else {
299 return Err(Error::Http(
300 StatusCode::UNAUTHORIZED,
301 format!("you do not have permission to impersonate other users"),
302 ));
303 }
304 }
305
306 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
307 let encrypted_access_token =
308 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
309
310 Ok(Json(CreateAccessTokenResponse {
311 user_id,
312 encrypted_access_token,
313 }))
314}
315
316async fn get_user_for_invite_code(
317 Path(code): Path<String>,
318 Extension(app): Extension<Arc<AppState>>,
319) -> Result<Json<User>> {
320 Ok(Json(app.db.get_user_for_invite_code(&code).await?))
321}