use crate::{ auth, db::{User, UserId}, rpc::ResultExt, AppState, Error, Result, }; use anyhow::anyhow; use axum::{ body::Body, extract::{Path, Query}, http::{self, Request, StatusCode}, middleware::{self, Next}, response::IntoResponse, routing::{get, post, put}, Extension, Json, Router, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tower::ServiceBuilder; use tracing::instrument; pub fn routes(rpc_server: &Arc, state: Arc) -> Router { Router::new() .route("/users", get(get_users).post(create_user)) .route( "/users/:id", put(update_user).delete(destroy_user).get(get_user), ) .route("/users/:id/access_tokens", post(create_access_token)) .route("/invite_codes/:code", get(get_user_for_invite_code)) .route("/panic", post(trace_panic)) .layer( ServiceBuilder::new() .layer(Extension(state)) .layer(Extension(rpc_server.clone())) .layer(middleware::from_fn(validate_api_token)), ) } pub async fn validate_api_token(req: Request, next: Next) -> impl IntoResponse { let token = req .headers() .get(http::header::AUTHORIZATION) .and_then(|header| header.to_str().ok()) .ok_or_else(|| { Error::Http( StatusCode::BAD_REQUEST, "missing authorization header".to_string(), ) })? .strip_prefix("token ") .ok_or_else(|| { Error::Http( StatusCode::BAD_REQUEST, "invalid authorization header".to_string(), ) })?; let state = req.extensions().get::>().unwrap(); if token != state.api_token { Err(Error::Http( StatusCode::UNAUTHORIZED, "invalid authorization token".to_string(), ))? } Ok::<_, Error>(next.run(req).await) } async fn get_users(Extension(app): Extension>) -> Result>> { let users = app.db.get_all_users().await?; Ok(Json(users)) } #[derive(Deserialize, Debug)] struct CreateUserParams { github_login: String, invite_code: Option, email_address: Option, admin: bool, } async fn create_user( Json(params): Json, Extension(app): Extension>, Extension(rpc_server): Extension>, ) -> Result> { println!("{:?}", params); let user_id = if let Some(invite_code) = params.invite_code { let invitee_id = app .db .redeem_invite_code( &invite_code, ¶ms.github_login, params.email_address.as_deref(), ) .await?; rpc_server .invite_code_redeemed(&invite_code, invitee_id) .await .trace_err(); invitee_id } else { app.db .create_user( ¶ms.github_login, params.email_address.as_deref(), params.admin, ) .await? }; let user = app .db .get_user_by_id(user_id) .await? .ok_or_else(|| anyhow!("couldn't find the user we just created"))?; Ok(Json(user)) } #[derive(Deserialize)] struct UpdateUserParams { admin: Option, invite_count: Option, } async fn update_user( Path(user_id): Path, Json(params): Json, Extension(app): Extension>, ) -> Result<()> { if let Some(admin) = params.admin { app.db.set_user_is_admin(UserId(user_id), admin).await?; } if let Some(invite_count) = params.invite_count { app.db .set_invite_count(UserId(user_id), invite_count) .await?; } Ok(()) } async fn destroy_user( Path(user_id): Path, Extension(app): Extension>, ) -> Result<()> { app.db.destroy_user(UserId(user_id)).await?; Ok(()) } async fn get_user( Path(login): Path, Extension(app): Extension>, ) -> Result> { let user = app .db .get_user_by_github_login(&login) .await? .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?; Ok(Json(user)) } #[derive(Debug, Deserialize)] struct Panic { version: String, text: String, } #[instrument(skip(panic))] async fn trace_panic(panic: Json) -> Result<()> { tracing::error!(version = %panic.version, text = %panic.text, "panic report"); Ok(()) } #[derive(Deserialize)] struct CreateAccessTokenQueryParams { public_key: String, impersonate: Option, } #[derive(Serialize)] struct CreateAccessTokenResponse { user_id: UserId, encrypted_access_token: String, } async fn create_access_token( Path(login): Path, Query(params): Query, Extension(app): Extension>, ) -> Result> { // request.require_token().await?; let user = app .db .get_user_by_github_login(&login) .await? .ok_or_else(|| anyhow!("user not found"))?; let mut user_id = user.id; if let Some(impersonate) = params.impersonate { if user.admin { if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { user_id = impersonated_user.id; } else { return Err(Error::Http( StatusCode::UNPROCESSABLE_ENTITY, format!("user {impersonate} does not exist"), )); } } else { return Err(Error::Http( StatusCode::UNAUTHORIZED, format!("you do not have permission to impersonate other users"), )); } } let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?; let encrypted_access_token = auth::encrypt_access_token(&access_token, params.public_key.clone())?; Ok(Json(CreateAccessTokenResponse { user_id, encrypted_access_token, })) } async fn get_user_for_invite_code( Path(code): Path, Extension(app): Extension>, ) -> Result> { Ok(Json(app.db.get_user_for_invite_code(&code).await?)) }