From 43c9b7c238d289cea0dc89e07ab377cebc7a23ec Mon Sep 17 00:00:00 2001 From: lucalise Date: Tue, 20 Jan 2026 15:58:10 -0800 Subject: [PATCH] feat: add repo validation --- api/src/endpoints/add_repo.rs | 8 ++------ api/src/endpoints/global_repos.rs | 8 ++++++++ api/src/endpoints/mod.rs | 1 + api/src/error.rs | 5 +++++ api/src/main.rs | 30 ++++++++++++++++++------------ api/src/user.rs | 20 +++++++++++++++++--- api/src/validate.rs | 27 +++++++++++++++++++++++++++ src/lib/Repos.svelte | 7 +++++-- 8 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 api/src/endpoints/global_repos.rs create mode 100644 api/src/validate.rs diff --git a/api/src/endpoints/add_repo.rs b/api/src/endpoints/add_repo.rs index c14b59a..971db42 100644 --- a/api/src/endpoints/add_repo.rs +++ b/api/src/endpoints/add_repo.rs @@ -1,19 +1,15 @@ -use crate::{auth::User, error::Result, user::RepositorySchema}; +use crate::{auth::User, error::Result, user::RepositorySchema, validate::validate_repo}; use actix_web::{HttpResponse, web}; use serde::Serialize; use crate::AppState; -#[derive(Serialize)] -struct AddResponse { - id: String, -} - pub async fn add_repo( app_state: web::Data, user: web::ReqData, payload: web::Json, ) -> Result { let repo = payload.into_inner(); + validate_repo(app_state.clone(), &repo).await?; app_state.user.add_repository(&user.id, repo).await } diff --git a/api/src/endpoints/global_repos.rs b/api/src/endpoints/global_repos.rs new file mode 100644 index 0000000..dbbb9da --- /dev/null +++ b/api/src/endpoints/global_repos.rs @@ -0,0 +1,8 @@ +use crate::error::Result; +use actix_web::{HttpResponse, web}; + +use crate::AppState; + +pub async fn global_repos(app_state: web::Data) -> Result { + Ok(HttpResponse::Ok().finish()) +} diff --git a/api/src/endpoints/mod.rs b/api/src/endpoints/mod.rs index 7d2a853..dd6524d 100644 --- a/api/src/endpoints/mod.rs +++ b/api/src/endpoints/mod.rs @@ -1,3 +1,4 @@ pub mod add_repo; pub mod get_repos; +pub mod global_repos; pub mod search_repos; diff --git a/api/src/error.rs b/api/src/error.rs index 73b0382..8ed81dd 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -23,6 +23,8 @@ pub enum Error { DynamoDB(String), #[error("item already exists")] AlreadyExists, + #[error("validation failed: {0}")] + ValidationFailed(String), } impl From> for Error { @@ -47,6 +49,9 @@ impl ResponseError for Error { Error::AlreadyExists => HttpResponse::BadRequest().json(ErrorResponse { error: "item already exists".to_string(), }), + Error::ValidationFailed(msg) => HttpResponse::BadRequest().json(ErrorResponse { + error: msg.clone(), + }), _ => HttpResponse::InternalServerError().finish(), } } diff --git a/api/src/main.rs b/api/src/main.rs index 4cef655..e9af6cd 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -3,6 +3,7 @@ mod endpoints; mod error; mod middleware; mod user; +mod validate; use std::env; @@ -72,18 +73,23 @@ async fn run() -> std::io::Result<()> { ) .service( web::scope("/api").service( - web::scope("/v0").service( - web::scope("/user") - .route("/repos", web::get().to(endpoints::get_repos::get_repos)) - .wrap(from_fn(middleware::protected)) - .route( - "/repos/search", - web::get().to(endpoints::search_repos::search_repos), - ) - .wrap(from_fn(middleware::protected)) - .route("/repo/add", web::post().to(endpoints::add_repo::add_repo)) - .wrap(from_fn(middleware::protected)), - ), + web::scope("/v0") + .service( + web::scope("/user") + .route("/repos", web::get().to(endpoints::get_repos::get_repos)) + .wrap(from_fn(middleware::protected)) + .route( + "/repos/search", + web::get().to(endpoints::search_repos::search_repos), + ) + .wrap(from_fn(middleware::protected)) + .route("/repo/add", web::post().to(endpoints::add_repo::add_repo)) + .wrap(from_fn(middleware::protected)), + ) + .route( + "/repos", + web::get().to(endpoints::global_repos::global_repos), + ), ), ) }) diff --git a/api/src/user.rs b/api/src/user.rs index 7c82a4f..3b25eb4 100644 --- a/api/src/user.rs +++ b/api/src/user.rs @@ -17,7 +17,7 @@ pub struct UserRepository { #[derive(Serialize, Deserialize)] pub struct RepositorySchema { pub id: String, - pub name: String, + pub full_name: String, } impl UserRepository { @@ -58,7 +58,7 @@ impl UserRepository { .ok()? .strip_prefix("REPO#")? .to_string(), - name: item.get("full_name")?.as_s().ok()?.to_string(), + full_name: item.get("full_name")?.as_s().ok()?.to_string(), }) }) .collect::>(); @@ -66,6 +66,20 @@ impl UserRepository { Ok(response) } + pub async fn global_repositories(&self) -> Result { + let response = self + .dynamodb_client + .query() + .key_condition_expression("pk = :pk") + .expression_attribute_values(":pk", AttributeValue::S("REPOS".into())) + .send() + .await? + .items() + .iter() + .filter_map(|item| Some(item.get("full_name")?.as_s().ok()?.to_string())) + .collect::>(); + } + pub async fn add_repository( &self, user_id: &str, @@ -79,7 +93,7 @@ impl UserRepository { .condition_expression("attribute_not_exists(sk)") .item("pk", AttributeValue::S(format!("USER#{user_id}"))) .item("sk", AttributeValue::S(format!("REPO#{}", repo.id))) - .item("full_name", AttributeValue::S(repo.name.to_string())) + .item("full_name", AttributeValue::S(repo.full_name.to_string())) .item("gsi1pk", AttributeValue::S("REPOS".into())) .item("gsi1sk", AttributeValue::S(now.clone())) .item("imported_at", AttributeValue::S(now)) diff --git a/api/src/validate.rs b/api/src/validate.rs new file mode 100644 index 0000000..e6d9577 --- /dev/null +++ b/api/src/validate.rs @@ -0,0 +1,27 @@ +use actix_web::web; + +use crate::AppState; +use crate::error::{Error, Result}; +use crate::user::RepositorySchema; + +pub async fn validate_repo(app_state: web::Data, repo: &RepositorySchema) -> Result<()> { + let response = app_state + .reqwest_client + .get(format!( + "https://raw.githubusercontent.com/{}/HEAD/dist/index.html", + repo.full_name + )) + .send() + .await?; + + match response.status() { + reqwest::StatusCode::OK => Ok(()), + reqwest::StatusCode::NOT_FOUND => Err(Error::ValidationFailed( + "dist/index.html not found in repository".to_string(), + )), + status => Err(Error::ValidationFailed(format!( + "failed to validate repository: HTTP {}", + status + ))), + } +} diff --git a/src/lib/Repos.svelte b/src/lib/Repos.svelte index fab2171..dcc18c2 100644 --- a/src/lib/Repos.svelte +++ b/src/lib/Repos.svelte @@ -73,11 +73,14 @@ }, body: JSON.stringify({ id: repo.id.toString(), - name: repo.full_name + full_name: repo.full_name }) }); + const data = await response.json(); if (!response.ok) { - toast.warning('Repository already imported'); + toast.warning(data.error); + adding = null; + return; } else { toast.success('Successfully added repository'); }