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