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