From 926bcdda9c25c87968e375f5c3a2855fe973057b Mon Sep 17 00:00:00 2001 From: Araozu Date: Tue, 29 Aug 2023 16:04:47 -0500 Subject: [PATCH] [BE][Certs] Connect to DB. Get person from DB or fake SUNAT API --- backend/.env.example | 2 + backend/.gitignore | 3 +- backend/.vscode/settings.json | 4 + backend/Cargo.lock | 11 ++- backend/Cargo.toml | 8 +- backend/sql/schema.sql | 11 +++ backend/src/controller/person/mod.rs | 101 ++++++++++++++++++++++--- backend/src/controller/register/mod.rs | 6 +- backend/src/cors.rs | 28 +++++++ backend/src/main.rs | 62 +++++++-------- backend/src/model/course.rs | 2 +- backend/src/model/mod.rs | 1 + backend/src/model/person.rs | 15 +++- backend/src/model/register.rs | 7 +- backend/src/model/reniec_person.rs | 64 ++++++++++++++++ 15 files changed, 269 insertions(+), 56 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/.vscode/settings.json create mode 100644 backend/sql/schema.sql create mode 100644 backend/src/cors.rs create mode 100644 backend/src/model/reniec_person.rs diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..61445d0 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL=mysql://user:password@localhost:3306/database +RENIEC_API=B8RT6dKlN5DF408cD5vds diff --git a/backend/.gitignore b/backend/.gitignore index 1de5659..c5dd462 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,2 @@ -target \ No newline at end of file +target +.env diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 0000000..701e292 --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.inlayHints.typeHints.enable": false, + "rust-analyzer.inlayHints.chainingHints.enable": false +} \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 12f838e..749534a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -102,8 +102,11 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "backend" version = "0.1.0" dependencies = [ + "dotenvy", + "once_cell", "reqwest", "rocket", + "serde", "sqlx", ] @@ -1719,18 +1722,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5b80dcf..1ee0d99 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = "0.11.20" +reqwest = { version = "0.11", features = ["json"] } rocket = { version = "=0.5.0-rc.3" , features = ["json", "msgpack", "uuid"] } -sqlx = { version = "0.7.1", features = [ "runtime-tokio", "tls-rustls" ] } - +sqlx = { version = "0.7.1", features = [ "runtime-tokio", "tls-rustls", "mysql", "macros" ] } +once_cell = "1.18.0" +dotenvy = "0.15.7" +serde = "1.0.188" diff --git a/backend/sql/schema.sql b/backend/sql/schema.sql new file mode 100644 index 0000000..b237682 --- /dev/null +++ b/backend/sql/schema.sql @@ -0,0 +1,11 @@ +# Mysql schema + +CREATE TABLE person ( + person_id INTEGER PRIMARY KEY AUTO_INCREMENT, + person_dni VARCHAR(8) NOT NULL, + person_names VARCHAR(50) NOT NULL, + person_paternal_surname VARCHAR(30) NOT NULL, + person_maternal_surname VARCHAR(30) NOT NULL +) + + diff --git a/backend/src/controller/person/mod.rs b/backend/src/controller/person/mod.rs index 7f8d288..9662538 100644 --- a/backend/src/controller/person/mod.rs +++ b/backend/src/controller/person/mod.rs @@ -1,15 +1,96 @@ +use rocket::http::Status; use rocket::serde::json::Json; +use reqwest::Client; -use crate::model::person::Person; +use crate::{db, model::person::Person}; +use crate::model::reniec_person::ReniecPerson; #[get("/person/")] -pub async fn get_by_dni(dni: i32) -> Json { - // TODO: get from database - Json(Person { - person_id: 1, - person_dni: format!("{}", dni), - person_names: "Juan".to_string(), - person_paternal_surname: "Perez".to_string(), - person_maternal_surname: "Gomez".to_string(), - }) +pub async fn get_by_dni(dni: i32) -> (Status, Json) { + let db = db(); + + /* + * Search person in DB + */ + let query = sqlx::query!("SELECT * FROM person WHERE person_dni = ?", dni) + .fetch_one(db) + .await; + + // Return if found + if let Ok(row) = query { + let person = Person { + person_id: row.person_id, + person_dni: row.person_dni, + person_names: row.person_names, + person_paternal_surname: row.person_paternal_surname, + person_maternal_surname: row.person_maternal_surname, + }; + + return (Status::Ok, Json(person)); + } + + /* + * Person not found in DB. Search in fake RENIEC API with reqwest + */ + let reniec_api_key = std::env::var("RENIEC_API").expect("RENIEC_API env var is not set!"); + let client = Client::new(); + let reqwest_result = client + .get(format!("https://api.apis.net.pe/v1/dni?numero={}", &dni)) + .bearer_auth(reniec_api_key) + .header("Content-Type", "application/json") + .header("charset", "utf-8") + .send() + .await; + + let reniec_person = match reqwest_result { + Ok(data) => { + let person = data.json::().await; + match person { + Ok(p) => Some(p), + Err(reason) => { + eprintln!( + "Error parsing data from fake RENIEC API into ReniecPerson: {:?}", + reason + ); + None + } + } + } + Err(error) => { + eprintln!("Error fetching person from fake RENIEC API: {:?}", error); + None + } + }; + + // If person is found in fake RENIEC API, insert to DB and return + if let Some(p) = reniec_person { + let query = sqlx::query!( + "INSERT INTO person (person_dni, person_names, person_paternal_surname, person_maternal_surname) VALUES (?, ?, ?, ?)", + dni, + p.nombres, + p.apellidoPaterno, + p.apellidoMaterno, + ) + .execute(db) + .await; + + if let Err(reason) = query { + eprintln!("Error inserting person into DB: {:?}", reason); + return (Status::InternalServerError, Json(Person::default())); + } + + let person = Person { + person_id: 0, + person_dni: format!("{}", dni), + person_names: p.nombres, + person_paternal_surname: p.apellidoPaterno, + person_maternal_surname: p.apellidoMaterno, + }; + + return (Status::Ok, Json(person)); + } + + + // Return error + (Status::NotFound, Json(Person::default())) } diff --git a/backend/src/controller/register/mod.rs b/backend/src/controller/register/mod.rs index 98c15b1..744030e 100644 --- a/backend/src/controller/register/mod.rs +++ b/backend/src/controller/register/mod.rs @@ -1,6 +1,6 @@ -use rocket::{serde::json::Json, http::Status}; +use rocket::{http::Status, serde::json::Json}; -use crate::model::register::{RegisterCreate, Register}; +use crate::model::register::{Register, RegisterCreate}; #[options("/register/batch")] pub fn options() -> Status { @@ -22,6 +22,7 @@ pub async fn get_by_dni(dni: i32) -> Json> { register_custom_label: "".to_string(), register_person_id: 1, register_course_id: 1, + register_is_preview: false, }; let r2 = Register { @@ -32,6 +33,7 @@ pub async fn get_by_dni(dni: i32) -> Json> { register_custom_label: "".to_string(), register_person_id: 1, register_course_id: 2, + register_is_preview: false, }; Json(vec![r1, r2]) diff --git a/backend/src/cors.rs b/backend/src/cors.rs new file mode 100644 index 0000000..e6225e4 --- /dev/null +++ b/backend/src/cors.rs @@ -0,0 +1,28 @@ +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::http::Header; + +pub struct Cors; + +#[rocket::async_trait] +impl Fairing for Cors { + fn info(&self) -> Info { + Info { + name: "CORS", + kind: Kind::Response, + } + } + + #[cfg(debug_assertions)] + async fn on_response<'r>( + &self, + _: &'r rocket::Request<'_>, + response: &mut rocket::Response<'r>, + ) { + response.set_header(Header::new("Access-Control-Allow-Origin", "*")); + response.set_header(Header::new( + "Access-Control-Allow-Methods", + "POST, GET, OPTIONS", + )); + response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type")); + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 0c72325..e44114f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,47 +1,49 @@ -use rocket::fairing::{Fairing, Info, Kind}; -use rocket::http::Header; +use cors::Cors; +use once_cell::sync::OnceCell; +use sqlx::mysql::MySqlPoolOptions; +use sqlx::{MySql, Pool}; +use std::env; #[macro_use] extern crate rocket; mod controller; +mod cors; mod model; -struct Cors; +static DB: OnceCell> = OnceCell::new(); -#[rocket::async_trait] -impl Fairing for Cors { - fn info(&self) -> Info { - Info { - name: "CORS", - kind: Kind::Response, - } - } - - #[cfg(debug_assertions)] - async fn on_response<'r>( - &self, - _: &'r rocket::Request<'_>, - response: &mut rocket::Response<'r>, - ) { - response.set_header(Header::new("Access-Control-Allow-Origin", "*")); - response.set_header(Header::new( - "Access-Control-Allow-Methods", - "POST, GET, OPTIONS", - )); - response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type")); - } +pub fn db() -> &'static Pool { + DB.get().expect("DB not initialized") } #[launch] -fn rocket() -> _ { - rocket::build() - .attach(Cors {}) - .mount("/api", routes![ +async fn rocket() -> _ { + dotenvy::dotenv().expect("Failed to load .env file"); + + /* + Init DB + */ + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let pool = MySqlPoolOptions::new() + .max_connections(5) + .connect(db_url.as_str()) + .await; + + match pool { + Ok(pool) => DB.set(pool).expect("Failed to set DB pool"), + Err(e) => println!("Error connecting to DB: {}", e), + } + + /* Init Rocket */ + rocket::build().attach(Cors {}).mount( + "/api", + routes![ controller::person::get_by_dni, controller::course::get_all, controller::register::insert_all, controller::register::options, controller::register::get_by_dni, - ]) + ], + ) } diff --git a/backend/src/model/course.rs b/backend/src/model/course.rs index bed1504..1dbea76 100644 --- a/backend/src/model/course.rs +++ b/backend/src/model/course.rs @@ -14,7 +14,7 @@ pub struct Course { pub course_display_name: String, /// Number of days of the course. /// Used to calculate how to space the courses - /// + /// /// Example: `2` pub course_days_amount: i32, /// Whether the course name can be extended with a label. diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs index 256ec51..2cff971 100644 --- a/backend/src/model/mod.rs +++ b/backend/src/model/mod.rs @@ -1,3 +1,4 @@ pub mod course; pub mod person; pub mod register; +pub mod reniec_person; diff --git a/backend/src/model/person.rs b/backend/src/model/person.rs index c662c5f..1254fad 100644 --- a/backend/src/model/person.rs +++ b/backend/src/model/person.rs @@ -1,7 +1,6 @@ -use rocket::serde::Serialize; +use serde::Serialize; #[derive(Serialize, Clone)] -#[serde(crate = "rocket::serde")] pub struct Person { /// Internal id pub person_id: i32, @@ -22,3 +21,15 @@ pub struct Person { /// Example: `Gomez` pub person_maternal_surname: String, } + +impl Person { + pub fn default() -> Person { + Person { + person_id: -1, + person_dni: "".to_string(), + person_names: "".to_string(), + person_paternal_surname: "".to_string(), + person_maternal_surname: "".to_string(), + } + } +} diff --git a/backend/src/model/register.rs b/backend/src/model/register.rs index 93a372b..ea94260 100644 --- a/backend/src/model/register.rs +++ b/backend/src/model/register.rs @@ -1,4 +1,4 @@ -use rocket::serde::{Serialize, Deserialize}; +use rocket::serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] #[serde(crate = "rocket::serde")] @@ -8,10 +8,9 @@ pub struct RegisterCreate { person_id: i32, course_id: i32, /// YYYY-MM-DD - date: String + date: String, } - #[derive(Serialize, Deserialize, Clone)] #[serde(crate = "rocket::serde")] pub struct Register { @@ -29,6 +28,8 @@ pub struct Register { /// Used in machinery courses, where the course name is the type of machinery, /// and the custom label the manufacturer and series (i.e. `320D CAT`). pub register_custom_label: String, + /// Whether this register is a preview of the certificate ("sin firmas") + pub register_is_preview: bool, /// Foreign key to the person table pub register_person_id: i32, /// Foreign key to the course table diff --git a/backend/src/model/reniec_person.rs b/backend/src/model/reniec_person.rs new file mode 100644 index 0000000..447104e --- /dev/null +++ b/backend/src/model/reniec_person.rs @@ -0,0 +1,64 @@ +use serde::{Deserialize, Serialize}; + +#[allow(non_snake_case)] +#[derive(Deserialize, Serialize, Clone)] +pub struct ReniecPerson { + /// Full name + /// + /// Example: `ADRIAN JORGE LOPEZ APAZA` + pub nombre: String, + /* + /// ? + tipoDocumento: i32, + /// DNI number + /// + /// Example: `12345678` + numeroDocumento: String, + /// ? + estado: String, + /// ? + condicion: String, + /// ? + direccion: String, + /// ? + ubigeo: String, + /// ? + viaTipo: String, + /// ? + viaNombre: String, + /// ? + zonaCodigo: String, + /// ? + zonaTipo: String, + /// ? + numero: String, + /// ? + interior: String, + /// ? + lote: String, + /// ? + dpto: String, + /// ? + manzana: String, + /// ? + kilometro: String, + /// ? + distrito: String, + /// ? + provincia: String, + /// ? + departamento: String, + /// First last name. + /// + */ + /// Example: `LOPEZ` + pub apellidoPaterno: String, + /// Second last name. + /// + /// Example: `APAZA` + pub apellidoMaterno: String, + /// All first names. + /// + /// Example: `ADRIAN JORGE` + pub nombres: String, +}