api.rs

  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            &params.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}