feat: add repo validation

This commit is contained in:
2026-01-20 15:58:10 -08:00
parent 856bde3d97
commit 43c9b7c238
8 changed files with 83 additions and 23 deletions

View File

@@ -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 actix_web::{HttpResponse, web};
use serde::Serialize; use serde::Serialize;
use crate::AppState; use crate::AppState;
#[derive(Serialize)]
struct AddResponse {
id: String,
}
pub async fn add_repo( pub async fn add_repo(
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
user: web::ReqData<User>, user: web::ReqData<User>,
payload: web::Json<RepositorySchema>, payload: web::Json<RepositorySchema>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let repo = payload.into_inner(); let repo = payload.into_inner();
validate_repo(app_state.clone(), &repo).await?;
app_state.user.add_repository(&user.id, repo).await app_state.user.add_repository(&user.id, repo).await
} }

View File

@@ -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<AppState>) -> Result<HttpResponse> {
Ok(HttpResponse::Ok().finish())
}

View File

@@ -1,3 +1,4 @@
pub mod add_repo; pub mod add_repo;
pub mod get_repos; pub mod get_repos;
pub mod global_repos;
pub mod search_repos; pub mod search_repos;

View File

@@ -23,6 +23,8 @@ pub enum Error {
DynamoDB(String), DynamoDB(String),
#[error("item already exists")] #[error("item already exists")]
AlreadyExists, AlreadyExists,
#[error("validation failed: {0}")]
ValidationFailed(String),
} }
impl<E: std::fmt::Debug> From<SdkError<E>> for Error { impl<E: std::fmt::Debug> From<SdkError<E>> for Error {
@@ -47,6 +49,9 @@ impl ResponseError for Error {
Error::AlreadyExists => HttpResponse::BadRequest().json(ErrorResponse { Error::AlreadyExists => HttpResponse::BadRequest().json(ErrorResponse {
error: "item already exists".to_string(), error: "item already exists".to_string(),
}), }),
Error::ValidationFailed(msg) => HttpResponse::BadRequest().json(ErrorResponse {
error: msg.clone(),
}),
_ => HttpResponse::InternalServerError().finish(), _ => HttpResponse::InternalServerError().finish(),
} }
} }

View File

@@ -3,6 +3,7 @@ mod endpoints;
mod error; mod error;
mod middleware; mod middleware;
mod user; mod user;
mod validate;
use std::env; use std::env;
@@ -72,18 +73,23 @@ async fn run() -> std::io::Result<()> {
) )
.service( .service(
web::scope("/api").service( web::scope("/api").service(
web::scope("/v0").service( web::scope("/v0")
web::scope("/user") .service(
.route("/repos", web::get().to(endpoints::get_repos::get_repos)) web::scope("/user")
.wrap(from_fn(middleware::protected)) .route("/repos", web::get().to(endpoints::get_repos::get_repos))
.route( .wrap(from_fn(middleware::protected))
"/repos/search", .route(
web::get().to(endpoints::search_repos::search_repos), "/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))
.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),
),
), ),
) )
}) })

View File

@@ -17,7 +17,7 @@ pub struct UserRepository {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct RepositorySchema { pub struct RepositorySchema {
pub id: String, pub id: String,
pub name: String, pub full_name: String,
} }
impl UserRepository { impl UserRepository {
@@ -58,7 +58,7 @@ impl UserRepository {
.ok()? .ok()?
.strip_prefix("REPO#")? .strip_prefix("REPO#")?
.to_string(), .to_string(),
name: item.get("full_name")?.as_s().ok()?.to_string(), full_name: item.get("full_name")?.as_s().ok()?.to_string(),
}) })
}) })
.collect::<Vec<RepositorySchema>>(); .collect::<Vec<RepositorySchema>>();
@@ -66,6 +66,20 @@ impl UserRepository {
Ok(response) Ok(response)
} }
pub async fn global_repositories(&self) -> Result<HttpResponse> {
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::<Vec<String>>();
}
pub async fn add_repository( pub async fn add_repository(
&self, &self,
user_id: &str, user_id: &str,
@@ -79,7 +93,7 @@ impl UserRepository {
.condition_expression("attribute_not_exists(sk)") .condition_expression("attribute_not_exists(sk)")
.item("pk", AttributeValue::S(format!("USER#{user_id}"))) .item("pk", AttributeValue::S(format!("USER#{user_id}")))
.item("sk", AttributeValue::S(format!("REPO#{}", repo.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("gsi1pk", AttributeValue::S("REPOS".into()))
.item("gsi1sk", AttributeValue::S(now.clone())) .item("gsi1sk", AttributeValue::S(now.clone()))
.item("imported_at", AttributeValue::S(now)) .item("imported_at", AttributeValue::S(now))

27
api/src/validate.rs Normal file
View File

@@ -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<AppState>, 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
))),
}
}

View File

@@ -73,11 +73,14 @@
}, },
body: JSON.stringify({ body: JSON.stringify({
id: repo.id.toString(), id: repo.id.toString(),
name: repo.full_name full_name: repo.full_name
}) })
}); });
const data = await response.json();
if (!response.ok) { if (!response.ok) {
toast.warning('Repository already imported'); toast.warning(data.error);
adding = null;
return;
} else { } else {
toast.success('Successfully added repository'); toast.success('Successfully added repository');
} }