1use crate::{
2 auth,
3 db::{User, UserId},
4 AppState, Error, Result,
5};
6use anyhow::anyhow;
7use axum::{
8 body::Body,
9 extract::{Path, Query},
10 http::{self, Request, StatusCode},
11 middleware::{self, Next},
12 response::IntoResponse,
13 routing::{get, post, put},
14 Extension, Json, Router,
15};
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use tower::ServiceBuilder;
19use tracing::instrument;
20
21pub fn routes(state: Arc<AppState>) -> Router<Body> {
22 Router::new()
23 .route("/users", get(get_users).post(create_user))
24 .route(
25 "/users/:id",
26 put(update_user).delete(destroy_user).get(get_user),
27 )
28 .route("/users/:id/access_tokens", post(create_access_token))
29 .route("/invite_codes/:code", get(get_user_for_invite_code))
30 .route("/panic", post(trace_panic))
31 .layer(
32 ServiceBuilder::new()
33 .layer(Extension(state))
34 .layer(middleware::from_fn(validate_api_token)),
35 )
36 // TODO: Compression on API routes?
37}
38
39pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
40 let token = req
41 .headers()
42 .get(http::header::AUTHORIZATION)
43 .and_then(|header| header.to_str().ok())
44 .ok_or_else(|| {
45 Error::Http(
46 StatusCode::BAD_REQUEST,
47 "missing authorization header".to_string(),
48 )
49 })?
50 .strip_prefix("token ")
51 .ok_or_else(|| {
52 Error::Http(
53 StatusCode::BAD_REQUEST,
54 "invalid authorization header".to_string(),
55 )
56 })?;
57
58 let state = req.extensions().get::<Arc<AppState>>().unwrap();
59
60 if token != state.api_token {
61 Err(Error::Http(
62 StatusCode::UNAUTHORIZED,
63 "invalid authorization token".to_string(),
64 ))?
65 }
66
67 Ok::<_, Error>(next.run(req).await)
68}
69
70async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
71 let users = app.db.get_all_users().await?;
72 Ok(Json(users))
73}
74
75#[derive(Deserialize)]
76struct CreateUserParams {
77 github_login: String,
78 invite_code: Option<String>,
79 admin: bool,
80}
81
82async fn create_user(
83 Json(params): Json<CreateUserParams>,
84 Extension(app): Extension<Arc<AppState>>,
85) -> Result<Json<User>> {
86 let user_id = if let Some(invite_code) = params.invite_code {
87 app.db
88 .redeem_invite_code(&invite_code, ¶ms.github_login)
89 .await?
90 } else {
91 app.db
92 .create_user(¶ms.github_login, params.admin)
93 .await?
94 };
95
96 let user = app
97 .db
98 .get_user_by_id(user_id)
99 .await?
100 .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
101
102 Ok(Json(user))
103}
104
105#[derive(Deserialize)]
106struct UpdateUserParams {
107 admin: Option<bool>,
108 invite_count: Option<u32>,
109}
110
111async fn update_user(
112 Path(user_id): Path<i32>,
113 Json(params): Json<UpdateUserParams>,
114 Extension(app): Extension<Arc<AppState>>,
115) -> Result<()> {
116 if let Some(admin) = params.admin {
117 app.db.set_user_is_admin(UserId(user_id), admin).await?;
118 }
119
120 if let Some(invite_count) = params.invite_count {
121 app.db
122 .set_invite_count(UserId(user_id), invite_count)
123 .await?;
124 }
125
126 Ok(())
127}
128
129async fn destroy_user(
130 Path(user_id): Path<i32>,
131 Extension(app): Extension<Arc<AppState>>,
132) -> Result<()> {
133 app.db.destroy_user(UserId(user_id)).await?;
134 Ok(())
135}
136
137async fn get_user(
138 Path(login): Path<String>,
139 Extension(app): Extension<Arc<AppState>>,
140) -> Result<Json<User>> {
141 let user = app
142 .db
143 .get_user_by_github_login(&login)
144 .await?
145 .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
146 Ok(Json(user))
147}
148
149#[derive(Debug, Deserialize)]
150struct Panic {
151 version: String,
152 text: String,
153}
154
155#[instrument(skip(panic))]
156async fn trace_panic(panic: Json<Panic>) -> Result<()> {
157 tracing::error!(version = %panic.version, text = %panic.text, "panic report");
158 Ok(())
159}
160
161#[derive(Deserialize)]
162struct CreateAccessTokenQueryParams {
163 public_key: String,
164 impersonate: Option<String>,
165}
166
167#[derive(Serialize)]
168struct CreateAccessTokenResponse {
169 user_id: UserId,
170 encrypted_access_token: String,
171}
172
173async fn create_access_token(
174 Path(login): Path<String>,
175 Query(params): Query<CreateAccessTokenQueryParams>,
176 Extension(app): Extension<Arc<AppState>>,
177) -> Result<Json<CreateAccessTokenResponse>> {
178 // request.require_token().await?;
179
180 let user = app
181 .db
182 .get_user_by_github_login(&login)
183 .await?
184 .ok_or_else(|| anyhow!("user not found"))?;
185
186 let mut user_id = user.id;
187 if let Some(impersonate) = params.impersonate {
188 if user.admin {
189 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
190 user_id = impersonated_user.id;
191 } else {
192 return Err(Error::Http(
193 StatusCode::UNPROCESSABLE_ENTITY,
194 format!("user {impersonate} does not exist"),
195 ));
196 }
197 } else {
198 return Err(Error::Http(
199 StatusCode::UNAUTHORIZED,
200 format!("you do not have permission to impersonate other users"),
201 ));
202 }
203 }
204
205 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
206 let encrypted_access_token =
207 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
208
209 Ok(Json(CreateAccessTokenResponse {
210 user_id,
211 encrypted_access_token,
212 }))
213}
214
215async fn get_user_for_invite_code(
216 Path(code): Path<String>,
217 Extension(app): Extension<Arc<AppState>>,
218) -> Result<Json<User>> {
219 Ok(Json(app.db.get_user_for_invite_code(&code).await?))
220}