api.rs

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