1pub mod contributors;
2pub mod events;
3pub mod extensions;
4pub mod ips_file;
5pub mod slack;
6
7use crate::{
8 auth,
9 db::{User, UserId},
10 rpc, AppState, Error, Result,
11};
12use anyhow::anyhow;
13use axum::{
14 body::Body,
15 extract::{Path, Query},
16 http::{self, Request, StatusCode},
17 middleware::{self, Next},
18 response::IntoResponse,
19 routing::{get, post},
20 Extension, Json, Router,
21};
22use axum_extra::response::ErasedJson;
23use serde::{Deserialize, Serialize};
24use std::sync::Arc;
25use tower::ServiceBuilder;
26
27pub use extensions::fetch_extensions_from_blob_store_periodically;
28
29pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Router<(), Body> {
30 Router::new()
31 .route("/user", get(get_authenticated_user))
32 .route("/users/:id/access_tokens", post(create_access_token))
33 .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
34 .merge(contributors::router())
35 .layer(
36 ServiceBuilder::new()
37 .layer(Extension(state))
38 .layer(Extension(rpc_server))
39 .layer(middleware::from_fn(validate_api_token)),
40 )
41}
42
43pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
44 let token = req
45 .headers()
46 .get(http::header::AUTHORIZATION)
47 .and_then(|header| header.to_str().ok())
48 .ok_or_else(|| {
49 Error::Http(
50 StatusCode::BAD_REQUEST,
51 "missing authorization header".to_string(),
52 )
53 })?
54 .strip_prefix("token ")
55 .ok_or_else(|| {
56 Error::Http(
57 StatusCode::BAD_REQUEST,
58 "invalid authorization header".to_string(),
59 )
60 })?;
61
62 let state = req.extensions().get::<Arc<AppState>>().unwrap();
63
64 if token != state.config.api_token {
65 Err(Error::Http(
66 StatusCode::UNAUTHORIZED,
67 "invalid authorization token".to_string(),
68 ))?
69 }
70
71 Ok::<_, Error>(next.run(req).await)
72}
73
74#[derive(Debug, Deserialize)]
75struct AuthenticatedUserParams {
76 github_user_id: Option<i32>,
77 github_login: String,
78 github_email: Option<String>,
79}
80
81#[derive(Debug, Serialize)]
82struct AuthenticatedUserResponse {
83 user: User,
84 metrics_id: String,
85}
86
87async fn get_authenticated_user(
88 Query(params): Query<AuthenticatedUserParams>,
89 Extension(app): Extension<Arc<AppState>>,
90) -> Result<Json<AuthenticatedUserResponse>> {
91 let initial_channel_id = app.config.auto_join_channel_id;
92
93 let user = app
94 .db
95 .get_or_create_user_by_github_account(
96 ¶ms.github_login,
97 params.github_user_id,
98 params.github_email.as_deref(),
99 initial_channel_id,
100 )
101 .await?;
102 let metrics_id = app.db.get_user_metrics_id(user.id).await?;
103 return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
104}
105
106#[derive(Deserialize, Debug)]
107struct CreateUserParams {
108 github_user_id: i32,
109 github_login: String,
110 email_address: String,
111 email_confirmation_code: Option<String>,
112 #[serde(default)]
113 admin: bool,
114 #[serde(default)]
115 invite_count: i32,
116}
117
118async fn get_rpc_server_snapshot(
119 Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
120) -> Result<ErasedJson> {
121 let Some(rpc_server) = rpc_server else {
122 return Err(Error::Internal(anyhow!("rpc server is not available")));
123 };
124
125 Ok(ErasedJson::pretty(rpc_server.snapshot().await))
126}
127
128#[derive(Deserialize)]
129struct CreateAccessTokenQueryParams {
130 public_key: String,
131 impersonate: Option<String>,
132}
133
134#[derive(Serialize)]
135struct CreateAccessTokenResponse {
136 user_id: UserId,
137 encrypted_access_token: String,
138}
139
140async fn create_access_token(
141 Path(user_id): Path<UserId>,
142 Query(params): Query<CreateAccessTokenQueryParams>,
143 Extension(app): Extension<Arc<AppState>>,
144) -> Result<Json<CreateAccessTokenResponse>> {
145 let user = app
146 .db
147 .get_user_by_id(user_id)
148 .await?
149 .ok_or_else(|| anyhow!("user not found"))?;
150
151 let mut impersonated_user_id = None;
152 if let Some(impersonate) = params.impersonate {
153 if user.admin {
154 if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
155 impersonated_user_id = Some(impersonated_user.id);
156 } else {
157 return Err(Error::Http(
158 StatusCode::UNPROCESSABLE_ENTITY,
159 format!("user {impersonate} does not exist"),
160 ));
161 }
162 } else {
163 return Err(Error::Http(
164 StatusCode::UNAUTHORIZED,
165 "you do not have permission to impersonate other users".to_string(),
166 ));
167 }
168 }
169
170 let access_token =
171 auth::create_access_token(app.db.as_ref(), user_id, impersonated_user_id).await?;
172 let encrypted_access_token =
173 auth::encrypt_access_token(&access_token, params.public_key.clone())?;
174
175 Ok(Json(CreateAccessTokenResponse {
176 user_id: impersonated_user_id.unwrap_or(user_id),
177 encrypted_access_token,
178 }))
179}