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