From 44a35e651741afb6c417da47d636e4380cdd225f Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sat, 13 Mar 2021 10:17:02 +0100 Subject: feat: add get all repos from db --- Cargo.lock | 18 +++++++++-- Cargo.toml | 11 ++++--- schema.sql | 31 +++++++++++++++++++ src/db.rs | 8 +++++ src/errors.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 16 +++++----- src/repository/mod.rs | 2 ++ src/repository/models.rs | 36 ++++++++++++++++++++++ src/repository/routes.rs | 23 ++++++++++++++ 9 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 schema.sql create mode 100644 src/db.rs create mode 100644 src/errors.rs create mode 100644 src/repository/mod.rs create mode 100644 src/repository/models.rs create mode 100644 src/repository/routes.rs diff --git a/Cargo.lock b/Cargo.lock index fb84b41..a9ca561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-trait" -version = "0.1.47" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e098e9c493fdf92832223594d9a164f96bdf17ba81a42aff86f85c76768726a" +checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" dependencies = [ "proc-macro2", "quote", @@ -491,6 +491,7 @@ dependencies = [ "libc", "num-integer", "num-traits 0.2.14", + "serde 1.0.124", "time", "winapi 0.3.9", ] @@ -908,6 +909,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-web", + "chrono", "config", "deadpool-postgres", "dotenv", @@ -918,6 +920,7 @@ dependencies = [ "tokio-pg-mapper", "tokio-pg-mapper-derive", "tokio-postgres", + "uuid", ] [[package]] @@ -1447,8 +1450,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfc08a7d94a80665de4a83942fa8db2fdeaf2f123fc0535e384dc4fff251efae" dependencies = [ "bytes 0.5.6", + "chrono", "fallible-iterator", "postgres-protocol", + "uuid", ] [[package]] @@ -2094,6 +2099,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "serde 1.0.124", +] + [[package]] name = "version_check" version = "0.9.2" diff --git a/Cargo.toml b/Cargo.toml index f0d9f2e..f57b112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ actix-web = "2.0.0" actix-http = "1.0.1" actix-service = "1.0.5" +tokio-pg-mapper = "0.1.4" +tokio-pg-mapper-derive = "0.1.4" +deadpool-postgres = "0.5.0" +tokio-postgres = { version = "0.5.1", features = ["with-uuid-0_8", "with-chrono-0_4"] } + slog = "2.4.1" slog-term = "2.5.0" slog-async = "2.4.0" @@ -17,7 +22,5 @@ slog-async = "2.4.0" dotenv = "0.15.0" config = "0.10.1" serde = { version = "1.0.104", features = ["derive"] } -deadpool-postgres = "0.5.0" -tokio-postgres = "0.5.1" -tokio-pg-mapper = "0.1.4" -tokio-pg-mapper-derive = "0.1" +chrono = { version = "0.4.19", features = ["serde"] } +uuid = { version = "0.8.2", features = ["serde"] } diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..9daa7f9 --- /dev/null +++ b/schema.sql @@ -0,0 +1,31 @@ +CREATE TABLE "repository" ( + id uuid PRIMARY KEY NOT NULL, + url varchar(255) UNIQUE NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + uploader_ip varchar(15) NOT NULL +); + +CREATE TABLE "email"( + email varchar(120) PRIMARY KEY NOT NULL, + hash_md5 varchar(32) UNIQUE NOT NULL +); + +CREATE TABLE "commit" ( + hash varchar(40) PRIMARY KEY NOT NULL, + tree varchar(40) REFERENCES commit(hash) NULL, + text text NOT NULL, + date timestamp NOT NULL, + author_email varchar(120) REFERENCES email(email) NOT NULL, + author_name varchar(120) NOT NULL, + committer_email varchar(120) REFERENCES email(email) NOT NULL, + committer_name varchar(120) NOT NULL, + repository_url varchar(256) REFERENCES repository(url) NOT NULL +); + +CREATE TABLE "branch" ( + id uuid PRIMARY KEY NOT NULL, + name varchar(120) NOT NULL, + repository_id uuid REFERENCES repository(id) NOT NULL, + head varchar(40) REFERENCES commit(hash) NULL +); diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..f547ab2 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,8 @@ +use crate::errors::AppError; +use deadpool_postgres::{Client, Pool, PoolError}; + +pub async fn get_client(pool: Pool) -> Result { + pool.get() + .await + .map_err(|err: PoolError| AppError::from(err)) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..ee3ca71 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,80 @@ +use actix_web::{error::ResponseError, http::StatusCode, HttpResponse}; +use deadpool_postgres::PoolError; +use serde::Serialize; +use std::fmt; +use tokio_postgres::error::Error; + +#[derive(Debug)] +pub enum AppErrorType { + DbError, + NotFoundError, +} + +#[derive(Debug)] +pub struct AppError { + pub message: Option, + pub cause: Option, + pub error_type: AppErrorType, +} + +impl AppError { + pub fn message(&self) -> String { + match &*self { + AppError { + message: Some(message), + .. + } => message.clone(), + AppError { + message: None, + error_type: AppErrorType::NotFoundError, + .. + } => "The requested item was not found".to_string(), + _ => "An unexpected error has occurred".to_string(), + } + } +} + +impl From for AppError { + fn from(error: PoolError) -> AppError { + AppError { + message: None, + cause: Some(error.to_string()), + error_type: AppErrorType::DbError, + } + } +} + +impl From for AppError { + fn from(error: Error) -> AppError { + AppError { + message: None, + cause: Some(error.to_string()), + error_type: AppErrorType::DbError, + } + } +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize)] +pub struct AppErrorResponse { + pub error: String, +} + +impl ResponseError for AppError { + fn status_code(&self) -> StatusCode { + match self.error_type { + AppErrorType::DbError => StatusCode::INTERNAL_SERVER_ERROR, + AppErrorType::NotFoundError => StatusCode::NOT_FOUND, + } + } + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(AppErrorResponse { + error: self.message(), + }) + } +} diff --git a/src/main.rs b/src/main.rs index 0b3d67f..176dae1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,15 @@ mod config; +mod db; +mod errors; -use actix_web::{middleware, web, App, HttpServer}; +mod repository; + +use actix_web::{middleware, App, HttpServer}; use dotenv::dotenv; use slog::info; use tokio_postgres::NoTls; -use crate::config::{Config, AppState}; - -async fn index(state: web::Data) -> &'static str { - info!(state.log, "GET `/` page"); - - "Hello from Rust!" -} +use crate::config::{AppState, Config}; #[actix_rt::main] async fn main() -> std::io::Result<()> { @@ -35,7 +33,7 @@ async fn main() -> std::io::Result<()> { log: log.clone(), }) .wrap(middleware::Logger::default()) - .route("/", web::get().to(index)) + .configure(repository::routes::config) }) .bind(format!("{}:{}", config.server.host, config.server.port))? .run() diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 0000000..a0e1883 --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod routes; diff --git a/src/repository/models.rs b/src/repository/models.rs new file mode 100644 index 0000000..ec5559b --- /dev/null +++ b/src/repository/models.rs @@ -0,0 +1,36 @@ +use crate::db::get_client; +use crate::errors::AppError; +use chrono::NaiveDateTime; +use deadpool_postgres::Pool; +use serde::{Deserialize, Serialize}; +use tokio_pg_mapper::FromTokioPostgresRow; +use tokio_pg_mapper_derive::PostgresMapper; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, PostgresMapper)] +#[pg_mapper(table = "repository")] +pub struct Repository { + pub id: Uuid, + pub url: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub uploader_ip: String, +} + +impl Repository { + pub async fn find_all(pool: Pool) -> Result, AppError> { + let client = get_client(pool.clone()).await.unwrap(); + let statement = client + .prepare("SELECT * FROM repository ORDER BY updated_at DESC") + .await?; + + let repos = client + .query(&statement, &[]) + .await? + .iter() + .map(|row| Repository::from_row_ref(row).unwrap()) + .collect::>(); + + Ok(repos) + } +} diff --git a/src/repository/routes.rs b/src/repository/routes.rs new file mode 100644 index 0000000..ebfff8e --- /dev/null +++ b/src/repository/routes.rs @@ -0,0 +1,23 @@ +use crate::config::AppState; +use crate::repository::models::Repository; +use actix_web::{web, HttpResponse, Responder}; +use slog::info; + +async fn index(state: web::Data) -> impl Responder { + let result = Repository::find_all(state.pool.clone()).await; + match result { + Ok(repos) => { + info!(state.log, "GET /repo/ 200"); + HttpResponse::Ok().json(repos) + } + _ => { + info!(state.log, "GET /repo/ 500"); + HttpResponse::BadRequest() + .body("Error trying to read all repositories from database") + } + } +} + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::resource("/repo/").route(web::get().to(index))); +} -- cgit v1.2.3-18-g5258 From 6a901654fac3952c953bafd4ac378ba1ebc648c3 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 00:37:03 +0100 Subject: chore: rename error key for responder --- src/errors.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index ee3ca71..082315e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -62,7 +62,7 @@ impl fmt::Display for AppError { #[derive(Serialize)] pub struct AppErrorResponse { - pub error: String, + pub detail: String, } impl ResponseError for AppError { @@ -74,7 +74,7 @@ impl ResponseError for AppError { } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).json(AppErrorResponse { - error: self.message(), + detail: self.message(), }) } } -- cgit v1.2.3-18-g5258 From 3232579f29d1c91e70e7dc76305d6589c4acbaae Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 00:39:27 +0100 Subject: chore: get repos error in json mode --- src/repository/routes.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/repository/routes.rs b/src/repository/routes.rs index ebfff8e..6b3a942 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -1,23 +1,21 @@ use crate::config::AppState; +use crate::errors::AppErrorResponse; use crate::repository::models::Repository; use actix_web::{web, HttpResponse, Responder}; use slog::info; async fn index(state: web::Data) -> impl Responder { let result = Repository::find_all(state.pool.clone()).await; + info!(state.log, "GET /repo/"); match result { - Ok(repos) => { - info!(state.log, "GET /repo/ 200"); - HttpResponse::Ok().json(repos) - } - _ => { - info!(state.log, "GET /repo/ 500"); - HttpResponse::BadRequest() - .body("Error trying to read all repositories from database") - } + Ok(repos) => HttpResponse::Ok().json(repos), + _ => HttpResponse::BadRequest().json(AppErrorResponse { + detail: "Error trying to read all repositories from database" + .to_string(), + }), } } pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/repo/").route(web::get().to(index))); + cfg.service(web::resource("/repo{_:/?}").route(web::get().to(index))) } -- cgit v1.2.3-18-g5258 From f7932c366bab2f5d0726f4d1cdf5b9dbeddf92eb Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 00:39:37 +0100 Subject: feat: find repo by id --- src/repository/models.rs | 23 ++++++++++++++++++++++- src/repository/routes.rs | 15 +++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/repository/models.rs b/src/repository/models.rs index ec5559b..54a93d7 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,5 +1,5 @@ use crate::db::get_client; -use crate::errors::AppError; +use crate::errors::{AppError, AppErrorType}; use chrono::NaiveDateTime; use deadpool_postgres::Pool; use serde::{Deserialize, Serialize}; @@ -33,4 +33,25 @@ impl Repository { Ok(repos) } + + pub async fn find(pool: Pool, id: &Uuid) -> Result { + let client = get_client(pool.clone()).await.unwrap(); + let statement = client + .prepare("SELECT * FROM repository WHERE id = $1") + .await?; + + let repo = client + .query_opt(&statement, &[&id]) + .await? + .map(|row| Repository::from_row_ref(&row).unwrap()); + + match repo { + Some(repo) => Ok(repo), + None => Err(AppError { + error_type: AppErrorType::NotFoundError, + cause: None, + message: Some("Repository not found".to_string()), + }), + } + } } diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 6b3a942..634d210 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -3,6 +3,7 @@ use crate::errors::AppErrorResponse; use crate::repository::models::Repository; use actix_web::{web, HttpResponse, Responder}; use slog::info; +use uuid::Uuid; async fn index(state: web::Data) -> impl Responder { let result = Repository::find_all(state.pool.clone()).await; @@ -16,6 +17,20 @@ async fn index(state: web::Data) -> impl Responder { } } +async fn get_repo( + state: web::Data, + id: web::Path<(Uuid,)>, +) -> impl Responder { + let result = Repository::find(state.pool.clone(), &id.0).await; + info!(state.log, "GET /repo/{}/", id.0); + result + .map(|repo| HttpResponse::Ok().json(repo)) + .map_err(|e| e) +} + pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("/repo{_:/?}").route(web::get().to(index))) + .service( + web::resource("/repo/{id}{_:/?}").route(web::get().to(get_repo)), + ); } -- cgit v1.2.3-18-g5258 From 6acc53da212e7506fd33534fd5cf9baa0d01e4ea Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 12:55:21 +0100 Subject: chore: add docs --- src/db.rs | 1 + src/errors.rs | 2 ++ src/repository/models.rs | 5 +++++ src/repository/routes.rs | 7 +++++++ 4 files changed, 15 insertions(+) diff --git a/src/db.rs b/src/db.rs index f547ab2..5367288 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,6 +1,7 @@ use crate::errors::AppError; use deadpool_postgres::{Client, Pool, PoolError}; +/// Return a valid `Client` to make SQL queries pub async fn get_client(pool: Pool) -> Result { pool.get() .await diff --git a/src/errors.rs b/src/errors.rs index 082315e..07df840 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -72,6 +72,8 @@ impl ResponseError for AppError { AppErrorType::NotFoundError => StatusCode::NOT_FOUND, } } + + /// Returns a JSON response with "detail" as key fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).json(AppErrorResponse { detail: self.message(), diff --git a/src/repository/models.rs b/src/repository/models.rs index 54a93d7..c480cbe 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -9,6 +9,7 @@ use uuid::Uuid; #[derive(Serialize, Deserialize, PostgresMapper)] #[pg_mapper(table = "repository")] +/// Repository model pub struct Repository { pub id: Uuid, pub url: String, @@ -18,6 +19,9 @@ pub struct Repository { } impl Repository { + /// Find all repositories inside the database. + /// Make a select query and order the repositories by descrescent updated + /// datetime pub async fn find_all(pool: Pool) -> Result, AppError> { let client = get_client(pool.clone()).await.unwrap(); let statement = client @@ -34,6 +38,7 @@ impl Repository { Ok(repos) } + /// Find a repository with an `id` equals to an Uuid element pub async fn find(pool: Pool, id: &Uuid) -> Result { let client = get_client(pool.clone()).await.unwrap(); let statement = client diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 634d210..0683b47 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -5,9 +5,12 @@ use actix_web::{web, HttpResponse, Responder}; use slog::info; use uuid::Uuid; +/// Endpoint used for retrieve all repositories async fn index(state: web::Data) -> impl Responder { let result = Repository::find_all(state.pool.clone()).await; info!(state.log, "GET /repo/"); + + // If raises an `Err`, returns an error in JSON format match result { Ok(repos) => HttpResponse::Ok().json(repos), _ => HttpResponse::BadRequest().json(AppErrorResponse { @@ -17,12 +20,16 @@ async fn index(state: web::Data) -> impl Responder { } } +/// Endpoint used for retrieve a repository that matches with an `id`. +/// It is a String, casted in an Uuid format. async fn get_repo( state: web::Data, id: web::Path<(Uuid,)>, ) -> impl Responder { let result = Repository::find(state.pool.clone(), &id.0).await; info!(state.log, "GET /repo/{}/", id.0); + + // `map_err` is also used when repo is not found result .map(|repo| HttpResponse::Ok().json(repo)) .map_err(|e| e) -- cgit v1.2.3-18-g5258 From 6afd0e59f62889d44b90193f0d8581668eae9c28 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 13:00:59 +0100 Subject: fix: parse uuid fields for get repo --- src/repository/routes.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 0683b47..56e42ab 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -24,9 +24,17 @@ async fn index(state: web::Data) -> impl Responder { /// It is a String, casted in an Uuid format. async fn get_repo( state: web::Data, - id: web::Path<(Uuid,)>, + id: web::Path<(String,)>, ) -> impl Responder { - let result = Repository::find(state.pool.clone(), &id.0).await; + // I have to match the &id.0 because if it's not a valid Uuid, the server + // must response "Repository not found". + // If I pass a not valid Uuid to Repository::find() it raises an error. + let uuid: Uuid = match Uuid::parse_str(&id.0) { + Ok(x) => x, + Err(_) => Uuid::parse_str("00000000000000000000000000000000").unwrap(), + }; + + let result = Repository::find(state.pool.clone(), &uuid).await; info!(state.log, "GET /repo/{}/", id.0); // `map_err` is also used when repo is not found -- cgit v1.2.3-18-g5258 From bf6c4bd75168671a5a0e53f7390ebd4796a6bd57 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 15:39:00 +0100 Subject: chore: add helpers module --- src/helpers.rs | 9 +++++++++ src/main.rs | 1 + src/repository/routes.rs | 6 ++---- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 src/helpers.rs diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..d915a50 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,9 @@ +use uuid::Uuid; + +/// Returns a valid Uuid if `id` is not a valid Uuid +pub fn uuid_from_string(id: &String) -> Uuid { + return match Uuid::parse_str(&id) { + Ok(x) => x, + Err(_) => Uuid::parse_str("00000000000000000000000000000000").unwrap(), + }; +} diff --git a/src/main.rs b/src/main.rs index 176dae1..2c5532a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod config; mod db; mod errors; +mod helpers; mod repository; diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 56e42ab..c13af7f 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -1,5 +1,6 @@ use crate::config::AppState; use crate::errors::AppErrorResponse; +use crate::helpers::uuid_from_string; use crate::repository::models::Repository; use actix_web::{web, HttpResponse, Responder}; use slog::info; @@ -29,10 +30,7 @@ async fn get_repo( // I have to match the &id.0 because if it's not a valid Uuid, the server // must response "Repository not found". // If I pass a not valid Uuid to Repository::find() it raises an error. - let uuid: Uuid = match Uuid::parse_str(&id.0) { - Ok(x) => x, - Err(_) => Uuid::parse_str("00000000000000000000000000000000").unwrap(), - }; + let uuid: Uuid = uuid_from_string(&id.0); let result = Repository::find(state.pool.clone(), &uuid).await; info!(state.log, "GET /repo/{}/", id.0); -- cgit v1.2.3-18-g5258 From 4b25bf3976b787bec97151c5c284f2fdf1f9c255 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 21:54:22 +0100 Subject: feat(errors): add authorizationerror in list --- src/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 07df840..8140cfe 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,6 +8,7 @@ use tokio_postgres::error::Error; pub enum AppErrorType { DbError, NotFoundError, + AuthorizationError, } #[derive(Debug)] @@ -70,6 +71,7 @@ impl ResponseError for AppError { match self.error_type { AppErrorType::DbError => StatusCode::INTERNAL_SERVER_ERROR, AppErrorType::NotFoundError => StatusCode::NOT_FOUND, + AppErrorType::AuthorizationError => StatusCode::UNAUTHORIZED, } } -- cgit v1.2.3-18-g5258 From 6b81a3cb99bc109726282d3e661f6b5ac5dde4c2 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 22:20:35 +0100 Subject: feat: add DELETE method --- README.md | 1 + src/repository/models.rs | 32 +++++++++++++++++++++++++++ src/repository/routes.rs | 57 +++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4b8d22..93de202 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,5 @@ PG.HOST= PG.PORT= PG.DBNAME= PG.POOL.MAX_SIZE= +SECRET_KEY= ``` diff --git a/src/repository/models.rs b/src/repository/models.rs index c480cbe..bcddcf1 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -59,4 +59,36 @@ impl Repository { }), } } + + /// Find a repository and delete it, but before check if "Authorization" + /// matches with SECRET_KEY + pub async fn delete( + pool: Pool, + id: &Uuid, + ) -> Result { + let client = get_client(pool.clone()).await.unwrap(); + let statement = client + .prepare( + " + DELETE FROM repository + WHERE id=$1 + RETURNING * + ", + ) + .await?; + + let repo = client + .query_opt(&statement, &[&id]) + .await? + .map(|row| Repository::from_row_ref(&row).unwrap()); + + match repo { + Some(repo) => Ok(repo), + None => Err(AppError { + error_type: AppErrorType::NotFoundError, + cause: None, + message: Some("Repository not found".to_string()), + }), + } + } } diff --git a/src/repository/routes.rs b/src/repository/routes.rs index c13af7f..54bb35d 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -1,9 +1,11 @@ use crate::config::AppState; -use crate::errors::AppErrorResponse; +use crate::errors::{AppError, AppErrorResponse, AppErrorType}; use crate::helpers::uuid_from_string; use crate::repository::models::Repository; -use actix_web::{web, HttpResponse, Responder}; +use actix_web::http::header; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; use slog::info; +use std::env; use uuid::Uuid; /// Endpoint used for retrieve all repositories @@ -41,9 +43,52 @@ async fn get_repo( .map_err(|e| e) } +/// Endpoint used for delete repository. +/// It uses a SECRET_KEY used like an API key +async fn delete_repo( + req: HttpRequest, + state: web::Data, + id: web::Path<(String,)>, +) -> impl Responder { + let uuid: Uuid = uuid_from_string(&id.0); + match req.headers().get(header::AUTHORIZATION) { + Some(x) + if x.to_str().unwrap() + != env::var("SECRET_KEY").unwrap_or("".to_string()) => + { + info!(state.log, "DELETE /repo/{}/ 401", id.0); + return Err(AppError { + error_type: AppErrorType::AuthorizationError, + message: Some( + "You must provide a valid Authorization".to_string(), + ), + cause: None, + }); + } + Some(_) => {} + None => { + info!(state.log, "DELETE /repo/{}/ 400", id.0); + return Ok(HttpResponse::BadRequest().body("")); + } + }; + + let result = Repository::delete(state.pool.clone(), &uuid).await; + info!(state.log, "DELETE /repo/{}/", id.0); + + result + .map(|_| HttpResponse::NoContent().body("")) + .map_err(|e| e) +} + +/// Routes for repository. TODO: create endpoint for UPDATE method pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/repo{_:/?}").route(web::get().to(index))) - .service( - web::resource("/repo/{id}{_:/?}").route(web::get().to(get_repo)), - ); + cfg.service( + web::scope("/repo") + .service(web::resource("{_:/?}").route(web::get().to(index))) + .service( + web::resource("/{id}{_:/?}") + .route(web::get().to(get_repo)) + .route(web::delete().to(delete_repo)), + ), + ); } -- cgit v1.2.3-18-g5258 From ff4a18ee44fb6b5907ad7016e41bbe61edc0bdcd Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sun, 14 Mar 2021 22:24:21 +0100 Subject: test: add insomnia for api testing --- extra/insomnia.yml | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 extra/insomnia.yml diff --git a/extra/insomnia.yml b/extra/insomnia.yml new file mode 100644 index 0000000..a682c95 --- /dev/null +++ b/extra/insomnia.yml @@ -0,0 +1,119 @@ +_type: export +__export_format: 4 +__export_date: 2021-03-14T21:23:45.742Z +__export_source: insomnia.desktop.app:v2020.4.2 +resources: + - _id: req_d42fbef765e149648399036fbb069ea3 + parentId: fld_b5ba3b39b5394b64b0fea47812192c27 + modified: 1615752358888 + created: 1615752333861 + url: "{{ API }}/repo/" + name: /repo/ + description: "" + method: GET + body: {} + parameters: [] + headers: [] + authentication: {} + metaSortKey: -1615752333861 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: fld_b5ba3b39b5394b64b0fea47812192c27 + parentId: wrk_83d42d64ffc54233ba18b02611268a92 + modified: 1615752305144 + created: 1615752305144 + name: Repository + description: "" + environment: {} + environmentPropertyOrder: null + metaSortKey: -1615752305144 + _type: request_group + - _id: wrk_83d42d64ffc54233ba18b02611268a92 + parentId: null + modified: 1615752298886 + created: 1615752298886 + name: Gico + description: "" + scope: null + _type: workspace + - _id: req_ad2a16371ab44200a3f580000ece7dec + parentId: fld_b5ba3b39b5394b64b0fea47812192c27 + modified: 1615752395118 + created: 1615752376746 + url: "{{API}}/repo/eac980b2-b4dc-400d-9110-4ea6a45c730a/" + name: /repo// + description: "" + method: GET + body: {} + parameters: [] + headers: [] + authentication: {} + metaSortKey: -1615752333811 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: req_0a203eacc036463a8e3c1a3a900b8109 + parentId: fld_b5ba3b39b5394b64b0fea47812192c27 + modified: 1615756966728 + created: 1615752431937 + url: "{{API}}/repo/dbc980b2-b4dc-400d-9110-4ea6a45c730a/" + name: /repo// + description: "" + method: DELETE + body: {} + parameters: [] + headers: [] + authentication: + type: bearer + token: this_is_THE_secret_key + prefix: "" + disabled: false + metaSortKey: -1615752333761 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: env_fae256ef6cd347e85cc3938afa45748d6a2ec5a2 + parentId: wrk_83d42d64ffc54233ba18b02611268a92 + modified: 1615752324694 + created: 1615752299371 + name: Base Environment + data: + API: http://localhost:5000 + dataPropertyOrder: + "&": + - API + color: null + isPrivate: false + metaSortKey: 1615752299371 + _type: environment + - _id: jar_fae256ef6cd347e85cc3938afa45748d6a2ec5a2 + parentId: wrk_83d42d64ffc54233ba18b02611268a92 + modified: 1615752299381 + created: 1615752299381 + name: Default Jar + cookies: [] + _type: cookie_jar + - _id: spc_b4597c0ad54448e7b3f35c845a6c0bd6 + parentId: wrk_83d42d64ffc54233ba18b02611268a92 + modified: 1615752298897 + created: 1615752298897 + fileName: Gico + contents: "" + contentType: yaml + _type: api_spec -- cgit v1.2.3-18-g5258 From 3273a9cc539ecf41029c2541a668c3ed24d9438f Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Tue, 16 Mar 2021 10:52:09 +0100 Subject: feat: use uuid v4 --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a9ca561..e579fb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,6 +2105,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ + "getrandom 0.2.2", "serde 1.0.124", ] diff --git a/Cargo.toml b/Cargo.toml index f57b112..fa12bb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,4 @@ dotenv = "0.15.0" config = "0.10.1" serde = { version = "1.0.104", features = ["derive"] } chrono = { version = "0.4.19", features = ["serde"] } -uuid = { version = "0.8.2", features = ["serde"] } +uuid = { version = "0.8.2", features = ["serde", "v4"] } -- cgit v1.2.3-18-g5258 From 8f853491ae808dd261cad5a053d5238181192e54 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Tue, 16 Mar 2021 10:54:20 +0100 Subject: feat: search repository helper --- src/repository/models.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/repository/models.rs b/src/repository/models.rs index bcddcf1..607008e 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,7 +1,7 @@ use crate::db::get_client; use crate::errors::{AppError, AppErrorType}; use chrono::NaiveDateTime; -use deadpool_postgres::Pool; +use deadpool_postgres::{Client, Pool}; use serde::{Deserialize, Serialize}; use tokio_pg_mapper::FromTokioPostgresRow; use tokio_pg_mapper_derive::PostgresMapper; @@ -91,4 +91,29 @@ impl Repository { }), } } + + /// Search a repository by its url + async fn search( + client: &Client, + url: String, + ) -> Result { + let statement = client + .prepare("SELECT * FROM repository WHERE url=$1") + .await?; + + let repo = client + .query_opt(&statement, &[&url]) + .await? + .map(|row| Repository::from_row_ref(&row).unwrap()); + + match repo { + Some(repo) => Ok(repo), + None => Err(AppError { + error_type: AppErrorType::NotFoundError, + cause: None, + message: Some("Repository not found".to_string()), + }), + } + } + } -- cgit v1.2.3-18-g5258 From 4048dd774c817462c0a692f0f94d979290e725ee Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Tue, 16 Mar 2021 10:54:28 +0100 Subject: feat: create repository --- src/repository/models.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ src/repository/routes.rs | 24 +++++++++++++++-- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/repository/models.rs b/src/repository/models.rs index 607008e..1fd1649 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,5 +1,6 @@ use crate::db::get_client; use crate::errors::{AppError, AppErrorType}; + use chrono::NaiveDateTime; use deadpool_postgres::{Client, Pool}; use serde::{Deserialize, Serialize}; @@ -7,6 +8,8 @@ use tokio_pg_mapper::FromTokioPostgresRow; use tokio_pg_mapper_derive::PostgresMapper; use uuid::Uuid; +use std::net::SocketAddr; + #[derive(Serialize, Deserialize, PostgresMapper)] #[pg_mapper(table = "repository")] /// Repository model @@ -18,6 +21,12 @@ pub struct Repository { pub uploader_ip: String, } +/// Struct used to create a new repository +#[derive(Serialize, Deserialize)] +pub struct RepositoryData { + pub url: String, +} + impl Repository { /// Find all repositories inside the database. /// Make a select query and order the repositories by descrescent updated @@ -116,4 +125,63 @@ impl Repository { } } + /// Create a new repository. It uses RepositoryData as support struct + pub async fn create( + pool: Pool, + data: &RepositoryData, + uploader_ip: Option, + ) -> Result { + let client = get_client(pool.clone()).await.unwrap(); + + // Search a repository that matches with that url, because if it's + // exists, the server do not create a clone + let repo_search = Repository::search(&client, data.url.clone()).await; + match repo_search { + Ok(_) => { + return Err(AppError { + message: Some("Repository already exists".to_string()), + cause: Some("".to_string()), + error_type: AppErrorType::AuthorizationError, + }); + } + Err(_) => {} + }; + + let statement = client + .prepare(" + INSERT INTO repository(id, url, uploader_ip) VALUES($1, $2, $3) RETURNING * + ").await?; + + // Create a new UUID v4 + let uuid = Uuid::new_v4(); + + // Match the uploader ip + let user_ip = match uploader_ip { + Some(ip) => ip.to_string(), + None => { + return Err(AppError { + message: Some("Failed to fetch uploader ip".to_string()), + cause: Some("".to_string()), + error_type: AppErrorType::AuthorizationError, + }) + } + }; + + let repo = client + .query(&statement, &[&uuid, &data.url, &user_ip]) + .await? + .iter() + .map(|row| Repository::from_row_ref(row).unwrap()) + .collect::>() + .pop(); + + match repo { + Some(repo) => Ok(repo), + None => Err(AppError { + message: Some("Error creating a new repository".to_string()), + cause: Some("Unknown error".to_string()), + error_type: AppErrorType::DbError, + }), + } + } } diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 54bb35d..a0f4db5 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -1,7 +1,7 @@ use crate::config::AppState; use crate::errors::{AppError, AppErrorResponse, AppErrorType}; use crate::helpers::uuid_from_string; -use crate::repository::models::Repository; +use crate::repository::models::{Repository, RepositoryData}; use actix_web::http::header; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use slog::info; @@ -80,11 +80,31 @@ async fn delete_repo( .map_err(|e| e) } +async fn create_repo( + req: HttpRequest, + payload: web::Json, + state: web::Data, +) -> impl Responder { + info!(state.log, "POST /repo/"); + let request_from_ip = HttpRequest::peer_addr(&req); + let result = + Repository::create(state.pool.clone(), &payload, request_from_ip) + .await; + + result + .map(|repo| HttpResponse::Ok().json(repo)) + .map_err(|e| e) +} + /// Routes for repository. TODO: create endpoint for UPDATE method pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/repo") - .service(web::resource("{_:/?}").route(web::get().to(index))) + .service( + web::resource("{_:/?}") + .route(web::get().to(index)) + .route(web::post().to(create_repo)), + ) .service( web::resource("/{id}{_:/?}") .route(web::get().to(get_repo)) -- cgit v1.2.3-18-g5258 From 6350610ef5f7d73680853d39898094f2bf15febb Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Tue, 16 Mar 2021 11:19:53 +0100 Subject: feat: make regex of url to check if it is valid Currently it works only with GitHub --- Cargo.lock | 1 + Cargo.toml | 1 + src/helpers.rs | 17 +++++++++++++++++ src/repository/models.rs | 16 ++++++++++++++-- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e579fb2..ad4fcfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,7 @@ dependencies = [ "config", "deadpool-postgres", "dotenv", + "regex", "serde 1.0.124", "slog", "slog-async", diff --git a/Cargo.toml b/Cargo.toml index fa12bb6..ae7576f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,4 @@ config = "0.10.1" serde = { version = "1.0.104", features = ["derive"] } chrono = { version = "0.4.19", features = ["serde"] } uuid = { version = "0.8.2", features = ["serde", "v4"] } +regex = "1" diff --git a/src/helpers.rs b/src/helpers.rs index d915a50..2dbacfd 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,3 +1,4 @@ +use regex::Regex; use uuid::Uuid; /// Returns a valid Uuid if `id` is not a valid Uuid @@ -7,3 +8,19 @@ pub fn uuid_from_string(id: &String) -> Uuid { Err(_) => Uuid::parse_str("00000000000000000000000000000000").unwrap(), }; } + +/// Check if a path is into the "valid git repositories" and returns the name +pub fn name_of_git_repository(url: &String) -> Option { + const GITHUB_RE: &str = r"^(http(s)?://)?(www.)?github.com/(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-]+)"; + let re = Regex::new(GITHUB_RE).unwrap(); + + if !re.is_match(&url) { + return None; + } + + let captures = re.captures(&url).unwrap(); + let name = captures.name("username").unwrap().as_str(); + let repo = captures.name("repository").unwrap().as_str(); + + Some(format!("{}/{}", name, repo)) +} diff --git a/src/repository/models.rs b/src/repository/models.rs index 1fd1649..1cbf3bb 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,5 +1,6 @@ use crate::db::get_client; use crate::errors::{AppError, AppErrorType}; +use crate::helpers::name_of_git_repository; use chrono::NaiveDateTime; use deadpool_postgres::{Client, Pool}; @@ -133,9 +134,20 @@ impl Repository { ) -> Result { let client = get_client(pool.clone()).await.unwrap(); + let repo_name: String = match name_of_git_repository(&data.url) { + Some(path) => path, + None => { + return Err(AppError { + message: Some("Repository not found".to_string()), + cause: Some("".to_string()), + error_type: AppErrorType::NotFoundError, + }); + } + }; + // Search a repository that matches with that url, because if it's // exists, the server do not create a clone - let repo_search = Repository::search(&client, data.url.clone()).await; + let repo_search = Repository::search(&client, repo_name.clone()).await; match repo_search { Ok(_) => { return Err(AppError { @@ -168,7 +180,7 @@ impl Repository { }; let repo = client - .query(&statement, &[&uuid, &data.url, &user_ip]) + .query(&statement, &[&uuid, &repo_name, &user_ip]) .await? .iter() .map(|row| Repository::from_row_ref(row).unwrap()) -- cgit v1.2.3-18-g5258