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