1use crate::{
2 auth,
3 db::{User, UserId},
4 rpc::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 serde::{Deserialize, Serialize};
18use std::sync::Arc;
19use tower::ServiceBuilder;
20use tracing::instrument;
21
22pub fn routes(rpc_server: &Arc<crate::rpc::Server>, state: Arc<AppState>) -> Router<Body> {
23 Router::new()
24 .route("/users", get(get_users).post(create_user))
25 .route(
26 "/users/:id",
27 put(update_user).delete(destroy_user).get(get_user),
28 )
29 .route("/users/:id/access_tokens", post(create_access_token))
30 .route("/invite_codes/:code", get(get_user_for_invite_code))
31 .route("/panic", post(trace_panic))
32 .layer(
33 ServiceBuilder::new()
34 .layer(Extension(state))
35 .layer(Extension(rpc_server.clone()))
36 .layer(middleware::from_fn(validate_api_token)),
37 )
38}
39
40pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
41 let token = req
42 .headers()
43 .get(http::header::AUTHORIZATION)
44 .and_then(|header| header.to_str().ok())
45 .ok_or_else(|| {
46 Error::Http(
47 StatusCode::BAD_REQUEST,
48 "missing authorization header".to_string(),
49 )
50 })?
51 .strip_prefix("token ")
52 .ok_or_else(|| {
53 Error::Http(
54 StatusCode::BAD_REQUEST,
55 "invalid authorization header".to_string(),
56 )
57 })?;
58
59 let state = req.extensions().get::<Arc<AppState>>().unwrap();
60
61 if token != state.api_token {
62 Err(Error::Http(
63 StatusCode::UNAUTHORIZED,
64 "invalid authorization token".to_string(),
65 ))?
66 }
67
68 Ok::<_, Error>(next.run(req).await)
69}
70
71async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
72 let users = app.db.get_all_users().await?;
73 Ok(Json(users))
74}
75
76#[derive(Deserialize, Debug)]
77struct CreateUserParams {
78 github_login: String,
79 invite_code: Option<String>,
80 email_address: Option<String>,
81 admin: bool,
82}
83
84async fn create_user(
85 Json(params): Json<CreateUserParams>,
86 Extension(app): Extension<Arc<AppState>>,
87 Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
88) -> Result<Json<User>> {
89 println!("{:?}", params);
90
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) -> Result<()> {
135 if let Some(admin) = params.admin {
136 app.db.set_user_is_admin(UserId(user_id), admin).await?;
137 }
138
139 if let Some(invite_count) = params.invite_count {
140 app.db
141 .set_invite_count(UserId(user_id), invite_count)
142 .await?;
143 }
144
145 Ok(())
146}
147
148async fn destroy_user(
149 Path(user_id): Path<i32>,
150 Extension(app): Extension<Arc<AppState>>,
151) -> Result<()> {
152 app.db.destroy_user(UserId(user_id)).await?;
153 Ok(())
154}
155
156async fn get_user(
157 Path(login): Path<String>,
158 Extension(app): Extension<Arc<AppState>>,
159) -> Result<Json<User>> {
160 let user = app
161 .db
162 .get_user_by_github_login(&login)
163 .await?
164 .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
165 Ok(Json(user))
166}
167
168#[derive(Debug, Deserialize)]
169struct Panic {
170 version: String,
171 text: String,
172}
173
174#[instrument(skip(panic))]
175async fn trace_panic(panic: Json<Panic>) -> Result<()> {
176 tracing::error!(version = %panic.version, text = %panic.text, "panic report");
177 Ok(())
178}
179
180#[derive(Deserialize)]
181struct CreateAccessTokenQueryParams {
182 public_key: String,
183 impersonate: Option<String>,
184}
185
186#[derive(Serialize)]
187struct CreateAccessTokenResponse {
188 user_id: UserId,
189 encrypted_access_token: String,
190}
191
192async fn create_access_token(
193 Path(login): Path<String>,
194 Query(params): Query<CreateAccessTokenQueryParams>,
195 Extension(app): Extension<Arc<AppState>>,
196) -> Result<Json<CreateAccessTokenResponse>> {
197 // request.require_token().await?;
198
199 let user = app
200 .db
201 .get_user_by_github_login(&login)
202 .await?
203 .ok_or_else(|| anyhow!("user not found"))?;
204
205 let mut user_id = user.id;
206 if let Some(impersonate) = params.impersonate {
207 if user.admin {
208 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
209 user_id = impersonated_user.id;
210 } else {
211 return Err(Error::Http(
212 StatusCode::UNPROCESSABLE_ENTITY,
213 format!("user {impersonate} does not exist"),
214 ));
215 }
216 } else {
217 return Err(Error::Http(
218 StatusCode::UNAUTHORIZED,
219 format!("you do not have permission to impersonate other users"),
220 ));
221 }
222 }
223
224 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
225 let encrypted_access_token =
226 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
227
228 Ok(Json(CreateAccessTokenResponse {
229 user_id,
230 encrypted_access_token,
231 }))
232}
233
234async fn get_user_for_invite_code(
235 Path(code): Path<String>,
236 Extension(app): Extension<Arc<AppState>>,
237) -> Result<Json<User>> {
238 Ok(Json(app.db.get_user_for_invite_code(&code).await?))
239}