api.rs

  1use crate::{
  2    auth,
  3    db::{ProjectId, User, UserId},
  4    rpc::{self, ResultExt},
  5    AppState, Error, Result,
  6};
  7use anyhow::anyhow;
  8use axum::{
  9    body::Body,
 10    extract::{Path, Query},
 11    http::{self, Request, StatusCode},
 12    middleware::{self, Next},
 13    response::IntoResponse,
 14    routing::{get, post, put},
 15    Extension, Json, Router,
 16};
 17use axum_extra::response::ErasedJson;
 18use serde::{Deserialize, Serialize};
 19use serde_json::json;
 20use std::sync::Arc;
 21use time::OffsetDateTime;
 22use tower::ServiceBuilder;
 23use tracing::instrument;
 24
 25pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
 26    Router::new()
 27        .route("/users", get(get_users).post(create_user))
 28        .route(
 29            "/users/:id",
 30            put(update_user).delete(destroy_user).get(get_user),
 31        )
 32        .route("/users/:id/access_tokens", post(create_access_token))
 33        .route("/bulk_users", post(create_users))
 34        .route("/users_with_no_invites", get(get_users_with_no_invites))
 35        .route("/invite_codes/:code", get(get_user_for_invite_code))
 36        .route("/panic", post(trace_panic))
 37        .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
 38        .route(
 39            "/project_activity_summary",
 40            get(get_project_activity_summary),
 41        )
 42        .route("/project_metadata", get(get_project_metadata))
 43        .layer(
 44            ServiceBuilder::new()
 45                .layer(Extension(state))
 46                .layer(Extension(rpc_server.clone()))
 47                .layer(middleware::from_fn(validate_api_token)),
 48        )
 49}
 50
 51pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
 52    let token = req
 53        .headers()
 54        .get(http::header::AUTHORIZATION)
 55        .and_then(|header| header.to_str().ok())
 56        .ok_or_else(|| {
 57            Error::Http(
 58                StatusCode::BAD_REQUEST,
 59                "missing authorization header".to_string(),
 60            )
 61        })?
 62        .strip_prefix("token ")
 63        .ok_or_else(|| {
 64            Error::Http(
 65                StatusCode::BAD_REQUEST,
 66                "invalid authorization header".to_string(),
 67            )
 68        })?;
 69
 70    let state = req.extensions().get::<Arc<AppState>>().unwrap();
 71
 72    if token != state.api_token {
 73        Err(Error::Http(
 74            StatusCode::UNAUTHORIZED,
 75            "invalid authorization token".to_string(),
 76        ))?
 77    }
 78
 79    Ok::<_, Error>(next.run(req).await)
 80}
 81
 82#[derive(Debug, Deserialize)]
 83struct GetUsersQueryParams {
 84    query: Option<String>,
 85    page: Option<u32>,
 86    limit: Option<u32>,
 87}
 88
 89async fn get_users(
 90    Query(params): Query<GetUsersQueryParams>,
 91    Extension(app): Extension<Arc<AppState>>,
 92) -> Result<Json<Vec<User>>> {
 93    let limit = params.limit.unwrap_or(100);
 94    let users = if let Some(query) = params.query {
 95        app.db.fuzzy_search_users(&query, limit).await?
 96    } else {
 97        app.db
 98            .get_all_users(params.page.unwrap_or(0), limit)
 99            .await?
100    };
101    Ok(Json(users))
102}
103
104#[derive(Deserialize, Debug)]
105struct CreateUserParams {
106    github_login: String,
107    invite_code: Option<String>,
108    email_address: Option<String>,
109    admin: bool,
110}
111
112async fn create_user(
113    Json(params): Json<CreateUserParams>,
114    Extension(app): Extension<Arc<AppState>>,
115    Extension(rpc_server): Extension<Arc<rpc::Server>>,
116) -> Result<Json<User>> {
117    let user_id = if let Some(invite_code) = params.invite_code {
118        let invitee_id = app
119            .db
120            .redeem_invite_code(
121                &invite_code,
122                &params.github_login,
123                params.email_address.as_deref(),
124            )
125            .await?;
126        rpc_server
127            .invite_code_redeemed(&invite_code, invitee_id)
128            .await
129            .trace_err();
130        invitee_id
131    } else {
132        app.db
133            .create_user(
134                &params.github_login,
135                params.email_address.as_deref(),
136                params.admin,
137            )
138            .await?
139    };
140
141    let user = app
142        .db
143        .get_user_by_id(user_id)
144        .await?
145        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
146
147    Ok(Json(user))
148}
149
150#[derive(Deserialize)]
151struct UpdateUserParams {
152    admin: Option<bool>,
153    invite_count: Option<u32>,
154}
155
156async fn update_user(
157    Path(user_id): Path<i32>,
158    Json(params): Json<UpdateUserParams>,
159    Extension(app): Extension<Arc<AppState>>,
160    Extension(rpc_server): Extension<Arc<rpc::Server>>,
161) -> Result<()> {
162    let user_id = UserId(user_id);
163
164    if let Some(admin) = params.admin {
165        app.db.set_user_is_admin(user_id, admin).await?;
166    }
167
168    if let Some(invite_count) = params.invite_count {
169        app.db.set_invite_count(user_id, invite_count).await?;
170        rpc_server.invite_count_updated(user_id).await.trace_err();
171    }
172
173    Ok(())
174}
175
176async fn destroy_user(
177    Path(user_id): Path<i32>,
178    Extension(app): Extension<Arc<AppState>>,
179) -> Result<()> {
180    app.db.destroy_user(UserId(user_id)).await?;
181    Ok(())
182}
183
184async fn get_user(
185    Path(login): Path<String>,
186    Extension(app): Extension<Arc<AppState>>,
187) -> Result<Json<User>> {
188    let user = app
189        .db
190        .get_user_by_github_login(&login)
191        .await?
192        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
193    Ok(Json(user))
194}
195
196#[derive(Deserialize)]
197struct CreateUsersParams {
198    users: Vec<CreateUsersEntry>,
199}
200
201#[derive(Deserialize)]
202struct CreateUsersEntry {
203    github_login: String,
204    email_address: String,
205    invite_count: usize,
206}
207
208async fn create_users(
209    Json(params): Json<CreateUsersParams>,
210    Extension(app): Extension<Arc<AppState>>,
211) -> Result<Json<Vec<User>>> {
212    let user_ids = app
213        .db
214        .create_users(
215            params
216                .users
217                .into_iter()
218                .map(|params| {
219                    (
220                        params.github_login,
221                        params.email_address,
222                        params.invite_count,
223                    )
224                })
225                .collect(),
226        )
227        .await?;
228    let users = app.db.get_users_by_ids(user_ids).await?;
229    Ok(Json(users))
230}
231
232#[derive(Debug, Deserialize)]
233struct GetUsersWithNoInvites {
234    invited_by_another_user: bool,
235}
236
237async fn get_users_with_no_invites(
238    Query(params): Query<GetUsersWithNoInvites>,
239    Extension(app): Extension<Arc<AppState>>,
240) -> Result<Json<Vec<User>>> {
241    Ok(Json(
242        app.db
243            .get_users_with_no_invites(params.invited_by_another_user)
244            .await?,
245    ))
246}
247
248#[derive(Debug, Deserialize)]
249struct Panic {
250    version: String,
251    text: String,
252}
253
254#[instrument(skip(panic))]
255async fn trace_panic(panic: Json<Panic>) -> Result<()> {
256    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
257    Ok(())
258}
259
260async fn get_rpc_server_snapshot(
261    Extension(rpc_server): Extension<Arc<rpc::Server>>,
262) -> Result<ErasedJson> {
263    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
264}
265
266#[derive(Deserialize)]
267struct GetProjectActivityParams {
268    #[serde(with = "time::serde::iso8601")]
269    start: OffsetDateTime,
270    #[serde(with = "time::serde::iso8601")]
271    end: OffsetDateTime,
272}
273
274async fn get_project_activity_summary(
275    Query(params): Query<GetProjectActivityParams>,
276    Extension(app): Extension<Arc<AppState>>,
277) -> Result<ErasedJson> {
278    let summary = app
279        .db
280        .summarize_project_activity(params.start..params.end, 100)
281        .await?;
282    Ok(ErasedJson::pretty(summary))
283}
284
285#[derive(Deserialize)]
286struct GetProjectMetadataParams {
287    project_id: u64,
288}
289
290async fn get_project_metadata(
291    Query(params): Query<GetProjectMetadataParams>,
292    Extension(app): Extension<Arc<AppState>>,
293) -> Result<ErasedJson> {
294    let extensions = app
295        .db
296        .get_project_extensions(ProjectId::from_proto(params.project_id))
297        .await?;
298    Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
299}
300
301#[derive(Deserialize)]
302struct CreateAccessTokenQueryParams {
303    public_key: String,
304    impersonate: Option<String>,
305}
306
307#[derive(Serialize)]
308struct CreateAccessTokenResponse {
309    user_id: UserId,
310    encrypted_access_token: String,
311}
312
313async fn create_access_token(
314    Path(login): Path<String>,
315    Query(params): Query<CreateAccessTokenQueryParams>,
316    Extension(app): Extension<Arc<AppState>>,
317) -> Result<Json<CreateAccessTokenResponse>> {
318    //     request.require_token().await?;
319
320    let user = app
321        .db
322        .get_user_by_github_login(&login)
323        .await?
324        .ok_or_else(|| anyhow!("user not found"))?;
325
326    let mut user_id = user.id;
327    if let Some(impersonate) = params.impersonate {
328        if user.admin {
329            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
330                user_id = impersonated_user.id;
331            } else {
332                return Err(Error::Http(
333                    StatusCode::UNPROCESSABLE_ENTITY,
334                    format!("user {impersonate} does not exist"),
335                ));
336            }
337        } else {
338            return Err(Error::Http(
339                StatusCode::UNAUTHORIZED,
340                format!("you do not have permission to impersonate other users"),
341            ));
342        }
343    }
344
345    let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?;
346    let encrypted_access_token =
347        auth::encrypt_access_token(&access_token, params.public_key.clone())?;
348
349    Ok(Json(CreateAccessTokenResponse {
350        user_id,
351        encrypted_access_token,
352    }))
353}
354
355async fn get_user_for_invite_code(
356    Path(code): Path<String>,
357    Extension(app): Extension<Arc<AppState>>,
358) -> Result<Json<User>> {
359    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
360}