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

View File

@@ -23,6 +23,8 @@ pub enum Error {
DynamoDB(String),
#[error("item already exists")]
AlreadyExists,
#[error("validation failed: {0}")]
ValidationFailed(String),
}
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: "item already exists".to_string(),
}),
Error::ValidationFailed(msg) => HttpResponse::BadRequest().json(ErrorResponse {
error: msg.clone(),
}),
_ => HttpResponse::InternalServerError().finish(),
}
}

View File

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

View File

@@ -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::<Vec<RepositorySchema>>();
@@ -66,6 +66,20 @@ impl UserRepository {
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(
&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))

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({
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');
}