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;
19
20pub fn routes(state: Arc<AppState>) -> Router<Body> {
21 Router::new()
22 .route("/users", get(get_users).post(create_user))
23 .route(
24 "/users/:id",
25 put(update_user).delete(destroy_user).get(get_user),
26 )
27 .route("/users/:id/access_tokens", post(create_access_token))
28 .layer(
29 ServiceBuilder::new()
30 .layer(Extension(state))
31 .layer(middleware::from_fn(validate_api_token)),
32 )
33 // TODO: Compression on API routes?
34}
35
36pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
37 let token = req
38 .headers()
39 .get(http::header::AUTHORIZATION)
40 .and_then(|header| header.to_str().ok())
41 .ok_or_else(|| {
42 Error::Http(
43 StatusCode::BAD_REQUEST,
44 "missing authorization header".to_string(),
45 )
46 })?
47 .strip_prefix("token ")
48 .ok_or_else(|| {
49 Error::Http(
50 StatusCode::BAD_REQUEST,
51 "invalid authorization header".to_string(),
52 )
53 })?;
54
55 let state = req.extensions().get::<Arc<AppState>>().unwrap();
56
57 if token != state.api_token {
58 Err(Error::Http(
59 StatusCode::UNAUTHORIZED,
60 "invalid authorization token".to_string(),
61 ))?
62 }
63
64 Ok::<_, Error>(next.run(req).await)
65}
66
67async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<User>>> {
68 let users = app.db.get_all_users().await?;
69 Ok(Json(users))
70}
71
72#[derive(Deserialize)]
73struct CreateUserParams {
74 github_login: String,
75 admin: bool,
76}
77
78async fn create_user(
79 Json(params): Json<CreateUserParams>,
80 Extension(app): Extension<Arc<AppState>>,
81) -> Result<Json<User>> {
82 let user_id = app
83 .db
84 .create_user(¶ms.github_login, params.admin)
85 .await?;
86
87 let user = app
88 .db
89 .get_user_by_id(user_id)
90 .await?
91 .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
92
93 Ok(Json(user))
94}
95
96#[derive(Deserialize)]
97struct UpdateUserParams {
98 admin: bool,
99}
100
101async fn update_user(
102 Path(user_id): Path<i32>,
103 Json(params): Json<UpdateUserParams>,
104 Extension(app): Extension<Arc<AppState>>,
105) -> Result<()> {
106 app.db
107 .set_user_is_admin(UserId(user_id), params.admin)
108 .await?;
109 Ok(())
110}
111
112async fn destroy_user(
113 Path(user_id): Path<i32>,
114 Extension(app): Extension<Arc<AppState>>,
115) -> Result<()> {
116 app.db.destroy_user(UserId(user_id)).await?;
117 Ok(())
118}
119
120async fn get_user(
121 Path(login): Path<String>,
122 Extension(app): Extension<Arc<AppState>>,
123) -> Result<Json<User>> {
124 let user = app
125 .db
126 .get_user_by_github_login(&login)
127 .await?
128 .ok_or_else(|| anyhow!("user not found"))?;
129 Ok(Json(user))
130}
131
132#[derive(Deserialize)]
133struct CreateAccessTokenQueryParams {
134 public_key: String,
135 impersonate: Option<String>,
136}
137
138#[derive(Serialize)]
139struct CreateAccessTokenResponse {
140 user_id: UserId,
141 encrypted_access_token: String,
142}
143
144async fn create_access_token(
145 Path(login): Path<String>,
146 Query(params): Query<CreateAccessTokenQueryParams>,
147 Extension(app): Extension<Arc<AppState>>,
148) -> Result<Json<CreateAccessTokenResponse>> {
149 // request.require_token().await?;
150
151 let user = app
152 .db
153 .get_user_by_github_login(&login)
154 .await?
155 .ok_or_else(|| anyhow!("user not found"))?;
156
157 let mut user_id = user.id;
158 if let Some(impersonate) = params.impersonate {
159 if user.admin {
160 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
161 user_id = impersonated_user.id;
162 } else {
163 return Err(Error::Http(
164 StatusCode::UNPROCESSABLE_ENTITY,
165 format!("user {impersonate} does not exist"),
166 ));
167 }
168 } else {
169 return Err(Error::Http(
170 StatusCode::UNAUTHORIZED,
171 format!("you do not have permission to impersonate other users"),
172 ));
173 }
174 }
175
176 let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
177 let encrypted_access_token =
178 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
179
180 Ok(Json(CreateAccessTokenResponse {
181 user_id,
182 encrypted_access_token,
183 }))
184}