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