[BE] Add comments
This commit is contained in:
parent
b8fc1bb951
commit
ee74e628ec
@ -1,5 +1,12 @@
|
|||||||
|
# URL de la base de datos. Se espera que sea Mysql
|
||||||
DATABASE_URL=mysql://user:password@localhost:3306/database
|
DATABASE_URL=mysql://user:password@localhost:3306/database
|
||||||
RENIEC_API=B8RT6dKlN5DF408cD5vds
|
# Clave API de Reniec
|
||||||
|
RENIEC_API=abcdefgh
|
||||||
|
# URL de la plataforma de aula virtual
|
||||||
CLASSROOM_URL=https://testing.aulavirtual.eegsac.com
|
CLASSROOM_URL=https://testing.aulavirtual.eegsac.com
|
||||||
|
# Usuario de la plataforma de aula virtual
|
||||||
CLASSROOM_USER=user
|
CLASSROOM_USER=user
|
||||||
|
# Contraseña de la plataforma de aula virtual
|
||||||
CLASSROOM_PASSWORD=password
|
CLASSROOM_PASSWORD=password
|
||||||
|
# Nivel de log de la aplicación
|
||||||
|
RUST_LOG=debug
|
||||||
|
114
backend/README.md
Normal file
114
backend/README.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# eeg-system: Backend
|
||||||
|
|
||||||
|
Programa escrito en Rust que contiene toda la lógica de negocios.
|
||||||
|
Expose únicamente rutas bajo `/api/`, todas se comunican mediante JSON.
|
||||||
|
|
||||||
|
Contiene el Modelo y Controlador de `MVC`. La vista está en la carpeta `frontend`.
|
||||||
|
|
||||||
|
## Lanzamiento
|
||||||
|
|
||||||
|
Para lanzar se requiere lo siguiente en el entorno:
|
||||||
|
|
||||||
|
- Sistema operativo Linux (no se desarrolló ni se probó el programa en windows/mac/bsd)
|
||||||
|
- Libreria Imagemagick instalada y accesible mediante el binario `convert`,
|
||||||
|
capaz de convertir archivos a PDF (en *buntu puede haber problemas de conversión,
|
||||||
|
ver [link en StackOverflow](https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion))
|
||||||
|
- Una carpeta `ESCANEOS` accesible, con permisos de lectura y escritura para
|
||||||
|
el usuario que ejecuta el backend. La ruta de esta carpeta está definida en
|
||||||
|
`/src/controller/scans/mod.rs`, en la variable `SCAN_PATH`
|
||||||
|
- Un archivo `.env` con las variables de entorno necesarias. Los nombres de las
|
||||||
|
claves están en el archivo `.env.example` de este repositorio
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Lanzamiento mediante Jenkins
|
||||||
|
|
||||||
|
Si se quiere lanzar mediante Jenkins ya hay un `Jenkinsfile` una carpeta arriba.
|
||||||
|
Modificar lo que sea necesario para ejecutar en el entorno objetivo.
|
||||||
|
|
||||||
|
|
||||||
|
## Funcionamiento
|
||||||
|
|
||||||
|
Básicamente, al iniciar el backend realiza lo siguiente:
|
||||||
|
|
||||||
|
- Cargar variables de entorno
|
||||||
|
- Inicializar el logger
|
||||||
|
- Conectar a la base de datos y crear un `pool` de conexiones
|
||||||
|
- Inicializar las rutas
|
||||||
|
|
||||||
|
Luego, las rutas brindan la siguiente funcionalidad:
|
||||||
|
|
||||||
|
- Buscar y crear personas
|
||||||
|
- CRUD de certificados
|
||||||
|
- Crear y gestionar accesos a aula virtual
|
||||||
|
- Automatizar escaneo de certificados
|
||||||
|
|
||||||
|
|
||||||
|
## Componentes del sistema
|
||||||
|
|
||||||
|
|
||||||
|
### `controller`
|
||||||
|
|
||||||
|
Los controladores, definen las rutas, reciben y envían datos al frontend
|
||||||
|
|
||||||
|
### `model`
|
||||||
|
|
||||||
|
Contiene todas las operaciones referentes a la base de datos.
|
||||||
|
|
||||||
|
El SQL se escribe manualmente, se usa la librería [sqlx](https://github.com/launchbadge/sqlx).
|
||||||
|
|
||||||
|
Sqxl brinda los sig. beneficios:
|
||||||
|
|
||||||
|
- Verificar tipos de datos correctos
|
||||||
|
- Verificar las consultas SQL
|
||||||
|
- Si hay algún problema en los tipos de datos o las consultas SQL, `sqlx` lanzará errores de compilación.
|
||||||
|
De este modo no es posible lanzar el backend si hay errores en el SQL.
|
||||||
|
|
||||||
|
Por estas razones no se usa un ORM (se intento SeaORM, es demasiado complicado y no brinda ningún beneficio
|
||||||
|
importante frente a sqlx).
|
||||||
|
|
||||||
|
|
||||||
|
### `online_classroom`
|
||||||
|
|
||||||
|
Contiene código que realiza scrapping del aula virtual de EEGSAC.
|
||||||
|
|
||||||
|
|
||||||
|
## Componentes dentro de `controller`
|
||||||
|
|
||||||
|
### `classroom`
|
||||||
|
|
||||||
|
Operaciones respecto al aula virtual. Se comunica con el aula virtual mediante scrapping.
|
||||||
|
|
||||||
|
### `course`
|
||||||
|
|
||||||
|
Devuelve una lista con todos los cursos del sistema
|
||||||
|
|
||||||
|
### `custom_label`
|
||||||
|
|
||||||
|
Devuelve una lista con las denominaciones de los certificados de maquinarias.
|
||||||
|
|
||||||
|
### `person`
|
||||||
|
|
||||||
|
CRUD de personas. La operación más importante es buscar una persona por DNI: Busca en la base de datos, si no encuentra
|
||||||
|
busca en API de RENIEC.
|
||||||
|
|
||||||
|
### `register`
|
||||||
|
|
||||||
|
CRUD de certificados.
|
||||||
|
|
||||||
|
### `scans`
|
||||||
|
|
||||||
|
Automatiza el escaneo de certificados. Realiza lo siguiente:
|
||||||
|
|
||||||
|
- Lee los archivos de la carpeta `ESCANEOS` de forma no recursiva.
|
||||||
|
- Cambia de nombre los archivos a `eeg_<unix timestamp>.jpg`. Esto se hace para que solo 1 persona pueda escanear
|
||||||
|
a la vez.
|
||||||
|
- Abre cada escaneo y busca el código QR: recorta la imagen a la sección donde se encuentra el QR, aplica [thresholding](https://en.wikipedia.org/wiki/Thresholding_%28image_processing%29) y detecta el QR.
|
||||||
|
- Según los datos del QR, busca en la base de datos nombres, apellidos y nombre del curso del escaneo
|
||||||
|
- Convierte a PDF, rota y renombra los escaneos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Las operaciones a más detalles están detalladas en el código.
|
||||||
|
|
@ -9,9 +9,13 @@ use std::{
|
|||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Move to ENV
|
||||||
const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";
|
const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";
|
||||||
|
|
||||||
/// Represents the result of parsing an eegsac URL
|
/// Represents the result of parsing a QR code, and look for an eegsac URL.
|
||||||
|
///
|
||||||
|
/// When the frontend requests the list of files available for conversion, this
|
||||||
|
/// is sent. It contains essentially a unix timestamp
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum ScanInfo {
|
pub enum ScanInfo {
|
||||||
/// The url has both DNI & id.
|
/// The url has both DNI & id.
|
||||||
@ -28,6 +32,7 @@ pub enum ScanInfo {
|
|||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads files from the `ESCANEOS` folder, and returns those that follow the rules
|
||||||
#[get("/scans/detect")]
|
#[get("/scans/detect")]
|
||||||
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<ScanInfo>>>) {
|
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<ScanInfo>>>) {
|
||||||
let files = match get_valid_files() {
|
let files = match get_valid_files() {
|
||||||
@ -82,6 +87,7 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
|
|
||||||
let file_path = p.path();
|
let file_path = p.path();
|
||||||
|
|
||||||
|
// Ignore directories
|
||||||
if file_path.is_dir() {
|
if file_path.is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -94,6 +100,7 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This may fail because filenames may contain non UTF-8 characters
|
||||||
let filename = match filename.to_str() {
|
let filename = match filename.to_str() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
@ -102,6 +109,7 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The rules for the filenames
|
||||||
if filename.starts_with("eeg") && filename.ends_with(".jpg") {
|
if filename.starts_with("eeg") && filename.ends_with(".jpg") {
|
||||||
result.push(file_path);
|
result.push(file_path);
|
||||||
};
|
};
|
||||||
@ -115,7 +123,38 @@ fn get_details_from_paths(paths: Vec<PathBuf>) -> Vec<ScanInfo> {
|
|||||||
paths.into_iter().map(|path| get_image_info(path)).collect()
|
paths.into_iter().map(|path| get_image_info(path)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Actually detects the QR data from a file.
|
||||||
|
/// Reads the file, looks for a QR & return info about this process
|
||||||
|
///
|
||||||
|
/// Here 3 things may happen:
|
||||||
|
///
|
||||||
|
/// - QR found
|
||||||
|
///
|
||||||
|
/// The URL is parsed to get the person DNI & certificate id.
|
||||||
|
///
|
||||||
|
/// The file in disk is renamed to `eeg_<timestamp>.jpg`
|
||||||
|
///
|
||||||
|
/// The timestamp, DNI & cert id (if found) are sent to FE
|
||||||
|
///
|
||||||
|
/// - QR not found
|
||||||
|
///
|
||||||
|
/// The file in disk is renamed to `eeg_<timestamp>.jpg`, and this timestamp is sent to FE
|
||||||
|
///
|
||||||
|
/// - Error finding QR
|
||||||
|
///
|
||||||
|
/// An error is returned
|
||||||
|
///
|
||||||
|
/// ## File renaming
|
||||||
|
///
|
||||||
|
/// The processed files are renamed to `eeg_<timestamp>.jpg` and sent to the FE.
|
||||||
|
///
|
||||||
|
/// Later on, the FE will send back this information, and another function in this
|
||||||
|
/// file will use the timestamps to read the JPGs, and do the conversion to PDF.
|
||||||
|
///
|
||||||
|
/// This is done so that there's no race conditions. If 2 persons call this function at
|
||||||
|
/// the same time, only the last one will have the valid timestamps.
|
||||||
fn get_image_info(path: PathBuf) -> ScanInfo {
|
fn get_image_info(path: PathBuf) -> ScanInfo {
|
||||||
|
// Open the image
|
||||||
let img = image::open(&path).unwrap();
|
let img = image::open(&path).unwrap();
|
||||||
|
|
||||||
// get unix timestamp now in ms
|
// get unix timestamp now in ms
|
||||||
@ -133,6 +172,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
let width = img.width();
|
let width = img.width();
|
||||||
let height = img.height();
|
let height = img.height();
|
||||||
|
|
||||||
|
// Calculate where to crop the image, such that only 1/8 is used
|
||||||
let third_point_height = (height / 4) * 3;
|
let third_point_height = (height / 4) * 3;
|
||||||
let mid_point_width = width / 2;
|
let mid_point_width = width / 2;
|
||||||
let remaining_height = height - third_point_height;
|
let remaining_height = height - third_point_height;
|
||||||
@ -140,17 +180,17 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
// crop image
|
// crop image
|
||||||
let cropped_img = img.crop_imm(0, third_point_height, mid_point_width, remaining_height);
|
let cropped_img = img.crop_imm(0, third_point_height, mid_point_width, remaining_height);
|
||||||
|
|
||||||
//
|
// Apply thresholding
|
||||||
// TODO: threshold cropped image before getting qr
|
|
||||||
//
|
|
||||||
let thresh_img = cropped_img.to_luma8();
|
let thresh_img = cropped_img.to_luma8();
|
||||||
let thresholded_image = imageproc::contrast::threshold(&thresh_img, 128);
|
let thresholded_image = imageproc::contrast::threshold(&thresh_img, 128);
|
||||||
// convert to image
|
|
||||||
let thresholded_image = image::DynamicImage::ImageLuma8(thresholded_image);
|
let thresholded_image = image::DynamicImage::ImageLuma8(thresholded_image);
|
||||||
|
|
||||||
// get qr from cropped image
|
// get qr from thresholded image.
|
||||||
|
// if the image is not thresholded, it may fail more often
|
||||||
let results = bardecoder::default_decoder().decode(&thresholded_image);
|
let results = bardecoder::default_decoder().decode(&thresholded_image);
|
||||||
|
|
||||||
|
|
||||||
|
// If no QR is detected, only rename
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
log::info!("QR not found");
|
log::info!("QR not found");
|
||||||
|
|
||||||
@ -165,7 +205,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
|
|
||||||
ScanInfo::Error("Error renombrando archivo.".into())
|
ScanInfo::Error("Error renombrando archivo.".into())
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = match &results[0] {
|
let url = match &results[0] {
|
||||||
@ -179,8 +219,11 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
// URL must have format `https://eegsac.com/certificado/<dni>?iid=<id>`
|
// URL must have format `https://eegsac.com/certificado/<dni>?iid=<id>`
|
||||||
// or `https://eegsac.com/certificado/<dni>`
|
// or `https://eegsac.com/certificado/<dni>`
|
||||||
|
|
||||||
|
// Try to get DNI & iid
|
||||||
match url.find('?') {
|
match url.find('?') {
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
|
// Here, an iid is found. Extract it & return
|
||||||
|
|
||||||
let dni_length = p - 31;
|
let dni_length = p - 31;
|
||||||
let equals_pos = match url.find('=') {
|
let equals_pos = match url.find('=') {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
@ -213,6 +256,8 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
// Here the URL has only a DNI, no iid
|
||||||
|
|
||||||
// Rename file
|
// Rename file
|
||||||
let mut new_path = path.clone();
|
let mut new_path = path.clone();
|
||||||
new_path.set_file_name(format!("eeg_{}.jpg", current_ms));
|
new_path.set_file_name(format!("eeg_{}.jpg", current_ms));
|
||||||
@ -229,8 +274,12 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should return a list of files that succeeded & failed
|
|
||||||
|
/// Converts a list of files into PDFs.
|
||||||
|
///
|
||||||
|
/// Uses the timestamps inside `data` to read the correct JPG files and convert them.
|
||||||
async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
||||||
|
// Get a tuple with all the DNIs & iids.
|
||||||
let (ids, dnis) = data.iter().fold(
|
let (ids, dnis) = data.iter().fold(
|
||||||
(vec!["-1".to_string()], vec!["''".to_string()]),
|
(vec!["-1".to_string()], vec!["''".to_string()]),
|
||||||
|mut acc, next| {
|
|mut acc, next| {
|
||||||
@ -244,12 +293,16 @@ async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Concatenate the iids & dnis
|
||||||
let register_id_list = ids.join(",");
|
let register_id_list = ids.join(",");
|
||||||
let person_dni_list = dnis.join(",");
|
let person_dni_list = dnis.join(",");
|
||||||
|
|
||||||
log::info!("register_id_list: {}", register_id_list);
|
log::info!("register_id_list: {}", register_id_list);
|
||||||
log::info!("person_dni_list: {}", person_dni_list);
|
log::info!("person_dni_list: {}", person_dni_list);
|
||||||
|
|
||||||
|
// This method expects the concatenated dnis & iids to build a single SQL query,
|
||||||
|
// instead of making a request for each file.
|
||||||
|
// Returns a list of information about each cert: person names, cert name
|
||||||
let persons_info = Register::get_course_and_person(register_id_list, person_dni_list).await;
|
let persons_info = Register::get_course_and_person(register_id_list, person_dni_list).await;
|
||||||
|
|
||||||
let persons_info = match persons_info {
|
let persons_info = match persons_info {
|
||||||
@ -287,7 +340,7 @@ async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScanInfo::Empty(timestamp) => {
|
ScanInfo::Empty(timestamp) => {
|
||||||
// This is dummy data, to convert files without QR
|
// This is dummy data, to convert files without a QR
|
||||||
let scan_data = ScanData {
|
let scan_data = ScanData {
|
||||||
course_name: "pendiente".into(),
|
course_name: "pendiente".into(),
|
||||||
person_names: "EEG".into(),
|
person_names: "EEG".into(),
|
||||||
@ -315,6 +368,7 @@ async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
let current_ms = current_time.as_millis();
|
let current_ms = current_time.as_millis();
|
||||||
|
|
||||||
|
// Create the resulting filename
|
||||||
let filename = format!(
|
let filename = format!(
|
||||||
"{} {} {} - {} [{}][{:X}].pdf",
|
"{} {} {} - {} [{}][{:X}].pdf",
|
||||||
person_info.person_names,
|
person_info.person_names,
|
||||||
@ -349,6 +403,7 @@ async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a file to PDF using Imagemagick
|
||||||
fn convert_to_pdf(image_path: &PathBuf, output_path: &PathBuf) -> Result<(), String> {
|
fn convert_to_pdf(image_path: &PathBuf, output_path: &PathBuf) -> Result<(), String> {
|
||||||
// Just use imagemagick to convert the image to pdf
|
// Just use imagemagick to convert the image to pdf
|
||||||
// -interlace Plane -gaussian-blur 0.05 -quality 75% -rotate 270 doc.jpg doc.jpg.pdf
|
// -interlace Plane -gaussian-blur 0.05 -quality 75% -rotate 270 doc.jpg doc.jpg.pdf
|
||||||
|
@ -3,6 +3,7 @@ use rocket::http::Header;
|
|||||||
|
|
||||||
pub struct Cors;
|
pub struct Cors;
|
||||||
|
|
||||||
|
/// This runs every request and adds the CORS headers to the response.
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl Fairing for Cors {
|
impl Fairing for Cors {
|
||||||
fn info(&self) -> Info {
|
fn info(&self) -> Info {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Represents a JSON response that can be either an error or a success
|
||||||
|
/// Is used as a helper to return JSON responses from the backend
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum JsonResult<A> {
|
pub enum JsonResult<A> {
|
||||||
Ok(A),
|
Ok(A),
|
||||||
|
@ -16,6 +16,9 @@ pub mod json_result;
|
|||||||
|
|
||||||
static DB: OnceCell<Pool<MySql>> = OnceCell::new();
|
static DB: OnceCell<Pool<MySql>> = OnceCell::new();
|
||||||
|
|
||||||
|
/// Returns a global reference to the database pool
|
||||||
|
/// This MUST be called after the DB pool has been initialized,
|
||||||
|
/// otherwise it will panic
|
||||||
pub fn db() -> &'static Pool<MySql> {
|
pub fn db() -> &'static Pool<MySql> {
|
||||||
DB.get().expect("DB not initialized")
|
DB.get().expect("DB not initialized")
|
||||||
}
|
}
|
||||||
@ -26,7 +29,7 @@ async fn rocket() -> _ {
|
|||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Init DB
|
Init DB and set it as a global variable
|
||||||
*/
|
*/
|
||||||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
|
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
|
||||||
let pool = MySqlPoolOptions::new()
|
let pool = MySqlPoolOptions::new()
|
||||||
@ -36,7 +39,7 @@ async fn rocket() -> _ {
|
|||||||
|
|
||||||
match pool {
|
match pool {
|
||||||
Ok(pool) => DB.set(pool).expect("Failed to set DB pool"),
|
Ok(pool) => DB.set(pool).expect("Failed to set DB pool"),
|
||||||
Err(e) => println!("Error connecting to DB: {}", e),
|
Err(e) => log::error!("Error connecting to DB: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init Rocket */
|
/* Init Rocket */
|
||||||
|
@ -2,6 +2,7 @@ use rocket::serde::Serialize;
|
|||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
|
||||||
|
/// Information about the available courses of EEGSAC
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct Course {
|
pub struct Course {
|
||||||
|
@ -3,11 +3,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
|
||||||
|
/// Information about a person registered in DB
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
/// Internal id
|
/// Internal id
|
||||||
pub person_id: i32,
|
pub person_id: i32,
|
||||||
/// Country-specific id. For now only supports Peru's DNI.
|
/// Country-specific id.
|
||||||
///
|
///
|
||||||
/// Example: `74185293`
|
/// Example: `74185293`
|
||||||
pub person_dni: String,
|
pub person_dni: String,
|
||||||
@ -24,6 +25,7 @@ pub struct Person {
|
|||||||
/// Example: `Gomez`
|
/// Example: `Gomez`
|
||||||
pub person_maternal_surname: String,
|
pub person_maternal_surname: String,
|
||||||
/// Id of the online classroom user id linked to this user
|
/// Id of the online classroom user id linked to this user
|
||||||
|
/// This is the user id found in the online classroom
|
||||||
pub person_classroom_id: Option<i32>,
|
pub person_classroom_id: Option<i32>,
|
||||||
pub person_classroom_username: Option<String>,
|
pub person_classroom_username: Option<String>,
|
||||||
}
|
}
|
||||||
@ -80,6 +82,7 @@ pub struct PersonLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PersonLink {
|
impl PersonLink {
|
||||||
|
/// Links a person to a user in the online classroom
|
||||||
pub async fn insert(&self) -> Result<(), String> {
|
pub async fn insert(&self) -> Result<(), String> {
|
||||||
let db = db();
|
let db = db();
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ pub struct RegisterCreate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterCreate {
|
impl RegisterCreate {
|
||||||
|
/// Registers a new certificate
|
||||||
pub async fn create(&self) -> Result<(), sqlx::Error> {
|
pub async fn create(&self) -> Result<(), sqlx::Error> {
|
||||||
let db = db();
|
let db = db();
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ impl RegisterCreate {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Each certificate has its own list of codes (for some reason),
|
||||||
|
// this method gets the next code
|
||||||
let next_register_code = Self::get_next_register_code(self.course_id).await?;
|
let next_register_code = Self::get_next_register_code(self.course_id).await?;
|
||||||
|
|
||||||
// Current date in YYYY-MM-DD format
|
// Current date in YYYY-MM-DD format
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// This is the data returned from the fake RENIEC API
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct ReniecPerson {
|
pub struct ReniecPerson {
|
||||||
|
@ -61,6 +61,11 @@ pub async fn create(data: &ClassroomPersonCreate) -> Result<PersonLink, String>
|
|||||||
Err("Expected empty body, found something else".into())
|
Err("Expected empty body, found something else".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Makes a request to the user creation form, and gets the
|
||||||
|
/// security token.
|
||||||
|
///
|
||||||
|
/// This security token must be present for CSRF reasons
|
||||||
async fn get_form_sec_token() -> Result<String, String> {
|
async fn get_form_sec_token() -> Result<String, String> {
|
||||||
let creation_form = request("/main/admin/user_add.php".into()).await?;
|
let creation_form = request("/main/admin/user_add.php".into()).await?;
|
||||||
|
|
||||||
@ -86,6 +91,8 @@ async fn get_form_sec_token() -> Result<String, String> {
|
|||||||
Ok(sec_token_value.into())
|
Ok(sec_token_value.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a request body data to send to the classroom
|
||||||
fn get_request_body(
|
fn get_request_body(
|
||||||
surnames: &String,
|
surnames: &String,
|
||||||
names: &String,
|
names: &String,
|
||||||
|
@ -7,6 +7,7 @@ pub struct ClassroomCourse {
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all the courses the user is enrolled in
|
||||||
pub async fn get_courses(user_id: i32) -> Result<Vec<ClassroomCourse>, String> {
|
pub async fn get_courses(user_id: i32) -> Result<Vec<ClassroomCourse>, String> {
|
||||||
let html = request(format!(
|
let html = request(format!(
|
||||||
"/main/admin/user_information.php?user_id={}",
|
"/main/admin/user_information.php?user_id={}",
|
||||||
|
@ -12,7 +12,14 @@ mod session;
|
|||||||
pub mod update_expiration_date;
|
pub mod update_expiration_date;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
/// Tries to connect to the online classroom, and get a session cookie
|
|
||||||
|
/// Tries to connect to the online classroom, and gets a session cookie
|
||||||
|
/// if neccesary.
|
||||||
|
///
|
||||||
|
/// Mantains a global session cookie for ~20 minutes.
|
||||||
|
///
|
||||||
|
/// Every time a request is made the session cookie is checked. If more than
|
||||||
|
/// 20 minutes have passed, a new session cookie is requested.
|
||||||
#[get("/classroom/connect")]
|
#[get("/classroom/connect")]
|
||||||
pub async fn connection() -> (Status, Json<JsonResult<()>>) {
|
pub async fn connection() -> (Status, Json<JsonResult<()>>) {
|
||||||
match ensure_session().await {
|
match ensure_session().await {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
/// Enrolls a user in a list of courses
|
||||||
pub async fn register_course(
|
pub async fn register_course(
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
surname_first_letter: &String,
|
surname_first_letter: &String,
|
||||||
|
@ -13,7 +13,7 @@ struct CookieHold {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Stores a client with a persistent cookie store
|
/// Stores a client with a persistent cookie store
|
||||||
static ref SESSION_COOKIE: RwLock<CookieHold> = RwLock::new(CookieHold { jar: CookieJar::new()});
|
static ref SESSION_COOKIE: RwLock<CookieHold> = RwLock::new(CookieHold { jar: CookieJar::new()});
|
||||||
/// Stores the last time a request was made, in seconds since UNIX epoch
|
/// Stores the last time a cookie was saved, in seconds since UNIX epoch
|
||||||
static ref SESSION_TIME: RwLock<u64> = RwLock::new(0);
|
static ref SESSION_TIME: RwLock<u64> = RwLock::new(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +165,9 @@ pub async fn register_courses_request(url: String, body: String) -> Result<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes sure that the session cookie is set, and that it is valid
|
/// Makes sure that the session cookie is set, and that it is valid.
|
||||||
|
///
|
||||||
|
/// If the cookie has expired, get a new one
|
||||||
pub async fn ensure_session() -> Result<(), String> {
|
pub async fn ensure_session() -> Result<(), String> {
|
||||||
let last_usage_time = SESSION_TIME.read().unwrap().clone();
|
let last_usage_time = SESSION_TIME.read().unwrap().clone();
|
||||||
|
|
||||||
@ -236,6 +238,9 @@ async fn login() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Helper function to log the responses recieved from the online classroom,
|
||||||
|
/// for debugging purposes
|
||||||
pub fn log_html(html: &String) {
|
pub fn log_html(html: &String) {
|
||||||
// Get current time and date in iso
|
// Get current time and date in iso
|
||||||
let now: DateTime<Local> = Local::now();
|
let now: DateTime<Local> = Local::now();
|
||||||
|
@ -2,6 +2,7 @@ use scraper::{Html, Selector};
|
|||||||
|
|
||||||
use super::session::{classroom_post_redirect, request};
|
use super::session::{classroom_post_redirect, request};
|
||||||
|
|
||||||
|
/// Sets the expiration date of the user account
|
||||||
pub async fn update_expiration_date(
|
pub async fn update_expiration_date(
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
new_expiration_date: String,
|
new_expiration_date: String,
|
||||||
|
Loading…
Reference in New Issue
Block a user