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
73async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
74 let users = app.db.get_all_users().await?;
75 Ok(Json(users))
76}
77
78#[derive(Deserialize, Debug)]
79struct CreateUserParams {
80 github_login: String,
81 invite_code: Option<String>,
82 email_address: Option<String>,
83 admin: bool,
84}
85
86async fn create_user(
87 Json(params): Json<CreateUserParams>,
88 Extension(app): Extension<Arc<AppState>>,
89 Extension(rpc_server): Extension<Arc<rpc::Server>>,
90) -> Result<Json<User>> {
91 let user_id = if let Some(invite_code) = params.invite_code {
92 let invitee_id = app
93 .db
94 .redeem_invite_code(
95 &invite_code,
96 ¶ms.github_login,
97 params.email_address.as_deref(),
98 )
99 .await?;
100 rpc_server
101 .invite_code_redeemed(&invite_code, invitee_id)
102 .await
103 .trace_err();
104 invitee_id
105 } else {
106 app.db
107 .create_user(
108 ¶ms.github_login,
109 params.email_address.as_deref(),
110 params.admin,
111 )
112 .await?
113 };
114
115 let user = app
116 .db
117 .get_user_by_id(user_id)
118 .await?
119 .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
120
121 Ok(Json(user))
122}
123
124#[derive(Deserialize)]
125struct UpdateUserParams {
126 admin: Option<bool>,
127 invite_count: Option<u32>,
128}
129
130async fn update_user(
131 Path(user_id): Path<i32>,
132 Json(params): Json<UpdateUserParams>,
133 Extension(app): Extension<Arc<AppState>>,
134 Extension(rpc_server): Extension<Arc<rpc::Server>>,
135) -> Result<()> {
136 let user_id = UserId(user_id);
137
138 if let Some(admin) = params.admin {
139 app.db.set_user_is_admin(user_id, admin).await?;
140 }
141
142 if let Some(invite_count) = params.invite_count {
143 app.db.set_invite_count(user_id, invite_count).await?;
144 rpc_server.invite_count_updated(user_id).await.trace_err();
145 }
146
147 Ok(())
148}
149
150async fn destroy_user(
151 Path(user_id): Path<i32>,
152 Extension(app): Extension<Arc<AppState>>,
153) -> Result<()> {
154 app.db.destroy_user(UserId(user_id)).await?;
155 Ok(())
156}
157
158async fn get_user(
159 Path(login): Path<String>,
160 Extension(app): Extension<Arc<AppState>>,
161) -> Result<Json<User>> {
162 let user = app
163 .db
164 .get_user_by_github_login(&login)
165 .await?
166 .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
167 Ok(Json(user))
168}
169
170#[derive(Debug, Deserialize)]
171struct Panic {
172 version: String,
173 text: String,
174}
175
176#[instrument(skip(panic))]
177async fn trace_panic(panic: Json<Panic>) -> Result<()> {
178 tracing::error!(version = %panic.version, text = %panic.text, "panic report");
179 Ok(())
180}
181
182async fn get_rpc_server_snapshot(
183 Extension(rpc_server): Extension<Arc<rpc::Server>>,
184) -> Result<ErasedJson> {
185 Ok(ErasedJson::pretty(rpc_server.snapshot().await))
186}
187
188#[derive(Deserialize)]
189struct CreateAccessTokenQueryParams {
190 public_key: String,
191 impersonate: Option<String>,
192}
193
194#[derive(Serialize)]
195struct CreateAccessTokenResponse {
196 user_id: UserId,
197 encrypted_access_token: String,
198}
199
200async fn create_access_token(
201 Path(login): Path<String>,
202 Query(params): Query<CreateAccessTokenQueryParams>,
203 Extension(app): Extension<Arc<AppState>>,
204) -> Result<Json<CreateAccessTokenResponse>> {
205 // request.require_token().await?;
206
207 let user = app
208 .db
209 .get_user_by_github_login(&login)
210 .await?
211 .ok_or_else(|| anyhow!("user not found"))?;
212
213 let mut user_id = user.id;
214 if let Some(impersonate) = params.impersonate {
215 if user.admin {
216 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
217 user_id = impersonated_user.id;
218 } else {
219 return Err(Error::Http(
220 StatusCode::UNPROCESSABLE_ENTITY,
221 format!("user {impersonate} does not exist"),
222 ));
223 }
224 } else {
225 return Err(Error::Http(
226 StatusCode::UNAUTHORIZED,
227 format!("you do not have permission to impersonate other users"),
228 ));
229 }
230 }
231
232 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
233 let encrypted_access_token =
234 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
235
236 Ok(Json(CreateAccessTokenResponse {
237 user_id,
238 encrypted_access_token,
239 }))
240}
241
242async fn get_user_for_invite_code(
243 Path(code): Path<String>,
244 Extension(app): Extension<Arc<AppState>>,
245) -> Result<Json<User>> {
246 Ok(Json(app.db.get_user_for_invite_code(&code).await?))
247}