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