Compare commits

...

10 Commits

17 changed files with 162 additions and 141 deletions

4
Jenkinsfile vendored
View File

@ -6,6 +6,10 @@ pipeline {
PATH = "/var/lib/jenkins/.cargo/bin:${env.PATH}" PATH = "/var/lib/jenkins/.cargo/bin:${env.PATH}"
} }
steps { steps {
// Clean workspace. TODO: Separate backend & frontend build
cleanWs()
checkout scm
dir("backend") { dir("backend") {
sh "touch .env" sh "touch .env"
sh "echo DATABASE_URL=mysql://educa7ls_user:123456789a*@md-89.webhostbox.net:3306/educa7ls_plataforma >> .env" sh "echo DATABASE_URL=mysql://educa7ls_user:123456789a*@md-89.webhostbox.net:3306/educa7ls_plataforma >> .env"

1
backend/.gitignore vendored
View File

@ -4,3 +4,4 @@ aulavirtual
scraps scraps
request-logs request-logs
.idea .idea
.directory

48
backend/Cargo.lock generated
View File

@ -158,7 +158,6 @@ dependencies = [
"isahc", "isahc",
"lazy_static", "lazy_static",
"log", "log",
"once_cell",
"regex", "regex",
"reqwest", "reqwest",
"rocket", "rocket",
@ -347,9 +346,9 @@ dependencies = [
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.17.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
"time 0.3.27", "time 0.3.27",
@ -1319,7 +1318,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown 0.12.3",
"serde",
] ]
[[package]] [[package]]
@ -1330,6 +1328,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.0", "hashbrown 0.14.0",
"serde",
] ]
[[package]] [[package]]
@ -1933,7 +1932,7 @@ checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c"
dependencies = [ dependencies = [
"inlinable_string", "inlinable_string",
"pear_codegen", "pear_codegen",
"yansi 1.0.0-rc.1", "yansi",
] ]
[[package]] [[package]]
@ -2162,7 +2161,7 @@ dependencies = [
"quote", "quote",
"syn 2.0.29", "syn 2.0.29",
"version_check", "version_check",
"yansi 1.0.0-rc.1", "yansi",
] ]
[[package]] [[package]]
@ -2456,9 +2455,9 @@ dependencies = [
[[package]] [[package]]
name = "rocket" name = "rocket"
version = "0.5.0-rc.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9" checksum = "9e7bb57ccb26670d73b6a47396c83139447b9e7878cab627fdfe9ea8da489150"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"async-trait", "async-trait",
@ -2468,8 +2467,7 @@ dependencies = [
"either", "either",
"figment", "figment",
"futures", "futures",
"indexmap 1.9.3", "indexmap 2.0.0",
"is-terminal",
"log", "log",
"memchr", "memchr",
"multer", "multer",
@ -2492,37 +2490,38 @@ dependencies = [
"ubyte", "ubyte",
"uuid", "uuid",
"version_check", "version_check",
"yansi 0.5.1", "yansi",
] ]
[[package]] [[package]]
name = "rocket_codegen" name = "rocket_codegen"
version = "0.5.0-rc.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b" checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c"
dependencies = [ dependencies = [
"devise", "devise",
"glob", "glob",
"indexmap 1.9.3", "indexmap 2.0.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rocket_http", "rocket_http",
"syn 2.0.29", "syn 2.0.29",
"unicode-xid", "unicode-xid",
"version_check",
] ]
[[package]] [[package]]
name = "rocket_http" name = "rocket_http"
version = "0.5.0-rc.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" checksum = "37a1663694d059fe5f943ea5481363e48050acedd241d46deb2e27f71110389e"
dependencies = [ dependencies = [
"cookie 0.17.0", "cookie 0.18.0",
"either", "either",
"futures", "futures",
"http", "http",
"hyper", "hyper",
"indexmap 1.9.3", "indexmap 2.0.0",
"log", "log",
"memchr", "memchr",
"pear", "pear",
@ -3183,9 +3182,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "state" name = "state"
version = "0.5.3" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [ dependencies = [
"loom", "loom",
] ]
@ -3962,17 +3961,14 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "1.0.0-rc.1" version = "1.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377"
dependencies = [
"is-terminal",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"

View File

@ -7,7 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
reqwest = { version = "0.11", features = ["json", "cookies"] } reqwest = { version = "0.11", features = ["json", "cookies"] }
rocket = { version = "=0.5.0-rc.3" , features = ["json", "msgpack", "uuid"] } rocket = { version = "0.5.0" , features = ["json", "msgpack", "uuid"] }
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "tls-rustls", "mysql", "macros", "chrono" ] } sqlx = { version = "0.7.1", features = [ "runtime-tokio", "tls-rustls", "mysql", "macros", "chrono" ] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
serde = "1.0.188" serde = "1.0.188"
@ -16,7 +16,6 @@ scraper = "0.17.1"
isahc = { version = "1.7.2", features = ["cookies"] } isahc = { version = "1.7.2", features = ["cookies"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
once_cell = "1.18.0"
log = "0.4.20" log = "0.4.20"
env_logger = "0.10.0" env_logger = "0.10.0"
bardecoder = "0.5.0" bardecoder = "0.5.0"

44
backend/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,44 @@
pipeline {
agent any
stages {
stage("Clean workspace") {
cleanWs()
checkout scm
}
stage("Build backend") {
environment {
PATH = "/var/lib/jenkins/.cargo/bin:${env.PATH}"
}
steps {
sh "touch .env"
sh "echo DATABASE_URL=mysql://educa7ls_user:123456789a*@md-89.webhostbox.net:3306/educa7ls_plataforma >> .env"
sh "echo RENIEC_API=apis-token-1.aTSI1U7KEuT-6bbbCguH-4Y8TI6KS73N >> .env"
sh "cargo build --release"
}
}
stage("Prepare docker") {
steps {
dir("docker") {
sh "touch .env"
sh "echo DATABASE_URL=mysql://educa7ls_user:123456789a*@md-89.webhostbox.net:3306/educa7ls_plataforma >> .env"
sh "echo RENIEC_API=apis-token-1.aTSI1U7KEuT-6bbbCguH-4Y8TI6KS73N >> .env"
sh "echo CLASSROOM_URL=https://aulavirtual.eegsac.com >> .env"
sh "echo CLASSROOM_USER=admin >> .env"
sh '''echo CLASSROOM_PASSWORD=YVL1@N4_PaL0-93\\$ >> .env'''
sh "echo RUST_LOG=info >> .env"
}
sh "cp ./target/release/backend ./docker"
}
}
stage("Start docker") {
steps {
dir("docker") {
sh "docker compose stop"
sh "docker compose up --build"
}
}
}
}
}

16
backend/docker/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
## This dockerfile just creates an image for the backend binary
## to run on.
## This Dockerfile expects on this same directory:
## - The backend binary
## - The .env file
# Copy the binary into the image
COPY backend .
# Copy the generated env file
COPY .env .
EXPOSE 8000
# Run the binary, the default Rocket port is 8000
CMD ["./backend"]

View File

@ -0,0 +1,11 @@
version: '3'
services:
com.eegsac.system:
container_name: com.eegsac.system
build:
context: .
dockerfile: Dockerfile
ports:
- "3333:8000"
restart: unless-stopped

View File

@ -12,8 +12,8 @@ use rocket::{http::Status, serde::json::Json};
mod courses; mod courses;
pub use courses::disenroll_user_options;
pub use courses::disenroll_user; pub use courses::disenroll_user;
pub use courses::disenroll_user_options;
#[options("/classroom/user")] #[options("/classroom/user")]
pub fn create_user_options() -> Status { pub fn create_user_options() -> Status {

View File

@ -4,6 +4,7 @@ use log::{error, info};
use reqwest::Client; use reqwest::Client;
use rocket::http::Status; use rocket::http::Status;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use sqlx::Connection;
use crate::json_result::JsonResult; use crate::json_result::JsonResult;
use crate::model::person::{PersonCreate, PersonLink}; use crate::model::person::{PersonCreate, PersonLink};

View File

@ -1,8 +1,6 @@
use cors::Cors; use cors::Cors;
use once_cell::sync::OnceCell; use sqlx::Connection;
use sqlx::mysql::MySqlPoolOptions; use sqlx::MySqlConnection;
use sqlx::MySql;
use sqlx::Pool;
use std::env; use std::env;
use std::time::Instant; use std::time::Instant;
@ -16,17 +14,15 @@ mod online_classroom;
pub mod json_result; pub mod json_result;
static DB: OnceCell<Pool<MySql>> = OnceCell::new();
/// Opens & returns a connection to the database /// Opens & returns a connection to the database
/// ///
/// We don't use a connection pool because it often times out, /// We don't use a connection pool because on some days, on the afternoon,
/// we don't have a lot of traffic and reiniting the pool would /// the connections die and the user has to wait for all 5 connections
/// require to change a lot of code in multiple places. /// in the pool to die and then wait for the new connections to be created.
pub async fn db() -> Result<&'static Pool<MySql>, String> { pub async fn db() -> Result<MySqlConnection, String> {
/* /*
Init DB and set it as a global variable Init DB and set it as a global variable
* / */
let db_url = match env::var("DATABASE_URL") { let db_url = match env::var("DATABASE_URL") {
Ok(url) => url, Ok(url) => url,
Err(_) => return Err("env DATABASE_URL not found".to_string()), Err(_) => return Err("env DATABASE_URL not found".to_string()),
@ -43,73 +39,6 @@ pub async fn db() -> Result<&'static Pool<MySql>, String> {
Err("Error connecting to DB".to_string()) Err("Error connecting to DB".to_string())
} }
} }
// */
let attempts = 3;
for _ in 0..attempts {
match DB.get() {
Some(db) => {
log::info!("DB active connections: {}", db.size());
log::info!("DB num_idle connections: {}", db.num_idle());
return Ok(db);
}
None => {
log::info!("DB not initialized, initializing from db()");
let _ = init_db().await;
}
}
}
log::error!("Failed to initialize DB after {} attempts", attempts);
Err("Failed to initialize DB".to_string())
}
pub async fn init_db() -> Result<(), String> {
/*
Init DB and set it as a global variable
*/
let db_url = match env::var("DATABASE_URL") {
Ok(url) => url,
Err(_) => return Err("env DATABASE_URL not found".to_string()),
};
let start = Instant::now();
let pool = MySqlPoolOptions::new()
.max_connections(5)
/*
On some afternoons for some god forsaken reason the idle connections
to the db stay alive, but stop responding.
When this happens, we must restart the server, or wait for all
the active connections to timeout.
Here are some measures to circumvent that:
*/
// Set the maximum wait time for connections to 10 seconds
// In practice, the slowest connections take 1.5 seconds to connect
.acquire_timeout(std::time::Duration::from_secs(10))
// Set the maximum idle time for connections to 10 minutes
.idle_timeout(std::time::Duration::from_secs(10 * 60))
.connect(db_url.as_str())
.await;
log::info!(
"DB Pool connection took: {:?} ms",
start.elapsed().as_millis()
);
match pool {
Ok(pool) => match DB.set(pool) {
Ok(_) => Ok(()),
Err(_) => {
log::error!("Failed to set DB global variable");
Err("Failed to set DB".to_string())
}
},
Err(e) => {
log::error!("Error connecting to DB: {}", e);
Err("Error connecting to DB".to_string())
}
}
} }
#[launch] #[launch]

View File

@ -35,7 +35,9 @@ impl Course {
} }
}; };
let results = sqlx::query!("SELECT * FROM course").fetch_all(db).await; let results = sqlx::query!("SELECT * FROM course")
.fetch_all(&mut db)
.await;
let results = match results { let results = match results {
Ok(res) => res, Ok(res) => res,
@ -71,7 +73,7 @@ impl Course {
"SELECT course_name FROM course WHERE course_id = ?", "SELECT course_name FROM course WHERE course_id = ?",
course_id course_id
) )
.fetch_one(db) .fetch_one(&mut db)
.await; .await;
match res { match res {

View File

@ -20,7 +20,7 @@ impl CustomLabel {
FROM custom_label FROM custom_label
"#, "#,
) )
.fetch_all(db) .fetch_all(&mut db)
.await; .await;
match result { match result {
@ -42,7 +42,7 @@ impl CustomLabel {
"SELECT custom_label_id FROM custom_label WHERE custom_label_value = ?", "SELECT custom_label_id FROM custom_label WHERE custom_label_value = ?",
value value
) )
.fetch_all(db) .fetch_all(&mut db)
.await; .await;
let result = match result { let result = match result {
@ -72,7 +72,7 @@ impl CustomLabel {
"INSERT INTO custom_label (custom_label_value) VALUES (?)", "INSERT INTO custom_label (custom_label_value) VALUES (?)",
value value
) )
.execute(db) .execute(&mut db)
.await; .await;
if let Err(err) = result { if let Err(err) = result {

View File

@ -36,14 +36,14 @@ pub struct Person {
impl Person { impl Person {
pub async fn get_by_dni(dni: i32) -> Result<Person, DBError> { pub async fn get_by_dni(dni: i32) -> Result<Person, DBError> {
let db = match db().await { let mut db = match db().await {
Ok(db) => db, Ok(db) => db,
Err(reason) => return Err(DBError::Str(reason)), Err(reason) => return Err(DBError::Str(reason)),
}; };
let start = Instant::now(); let start = Instant::now();
let result = sqlx::query_as!(Person, "SELECT * FROM person WHERE person_dni = ?", dni) let result = sqlx::query_as!(Person, "SELECT * FROM person WHERE person_dni = ?", dni)
.fetch_one(db) .fetch_one(&mut db)
.await; .await;
log::info!( log::info!(
"DB query (person by dni) took: {:?} ms", "DB query (person by dni) took: {:?} ms",
@ -82,7 +82,7 @@ impl PersonCreate {
self.person_paternal_surname, self.person_paternal_surname,
self.person_maternal_surname, self.person_maternal_surname,
) )
.execute(db) .execute(&mut db)
.await; .await;
match result { match result {
@ -116,7 +116,7 @@ impl PersonLink {
self.person_classroom_username, self.person_classroom_username,
self.person_id, self.person_id,
) )
.execute(db) .execute(&mut db)
.await; .await;
match res { match res {

View File

@ -83,7 +83,7 @@ impl RegisterCreate {
self.person_id, self.person_id,
self.course_id self.course_id
) )
.execute(db) .execute(&mut db)
.await; .await;
match result { match result {
@ -107,7 +107,7 @@ impl RegisterCreate {
WHERE register_course_id IN WHERE register_course_id IN
(SELECT course_id FROM course WHERE course_name LIKE 'Matpel%')", (SELECT course_id FROM course WHERE course_name LIKE 'Matpel%')",
) )
.fetch_one(db) .fetch_one(&mut db)
.await; .await;
match res { match res {
@ -126,7 +126,7 @@ impl RegisterCreate {
WHERE register_course_id=?", WHERE register_course_id=?",
course_id course_id
) )
.fetch_one(db) .fetch_one(&mut db)
.await; .await;
match res { match res {
@ -345,7 +345,7 @@ impl Register {
WHERE register_person_id = (SELECT person_id FROM person WHERE person_dni = ?)", WHERE register_person_id = (SELECT person_id FROM person WHERE person_dni = ?)",
dni dni
) )
.fetch_all(db) .fetch_all(&mut db)
.await; .await;
log::info!( log::info!(
"DB (get register by id) took: {:?} ms", "DB (get register by id) took: {:?} ms",
@ -381,7 +381,7 @@ impl Register {
let mut db = db().await?; let mut db = db().await?;
let res = sqlx::query!("DELETE FROM register WHERE register_id = ?", register_id) let res = sqlx::query!("DELETE FROM register WHERE register_id = ?", register_id)
.execute(db) .execute(&mut db)
.await; .await;
match res { match res {

View File

@ -103,6 +103,9 @@ pub async fn request(url: String) -> Result<String, String> {
// Get the stored client // Get the stored client
let jar = SESSION_COOKIE.read().unwrap().jar.clone(); let jar = SESSION_COOKIE.read().unwrap().jar.clone();
log::info!("Classroom request: url: {}", url);
log::info!("Classroom request: cookie jar: {:?}", jar);
let uri = format!("{}{}", classroom_url, url); let uri = format!("{}{}", classroom_url, url);
// Do the request // Do the request
@ -252,11 +255,23 @@ async fn login() -> Result<(), String> {
match response { match response {
Ok(mut r) => { Ok(mut r) => {
if r.status() == isahc::http::StatusCode::FOUND { if r.status() == isahc::http::StatusCode::FOUND {
// TODO: Even if this is a 302, it might not be a successful login let html = match r.text() {
Ok(t) => t,
Err(err) => {
return Err(format!("Error getting text from login response: {:?}", err))
}
};
log::info!("classroom login: 302 response.");
log_html(&html);
log::info!("classroom login cookies: {:?}", jar);
// check Set-Cookie header // check Set-Cookie header
SESSION_COOKIE.write().unwrap().jar = jar.clone(); SESSION_COOKIE.write().unwrap().jar = jar.clone();
Ok(()) Ok(())
} else { } else {
log::info!("classroom login: not 302");
// Write html to file // Write html to file
match r.text() { match r.text() {
Ok(t) => { Ok(t) => {

1
frontend/.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules node_modules
dist dist
.directory

View File

@ -32,20 +32,22 @@ body {
.progress { .progress {
animation: progress 1s infinite linear; animation: progress 1s infinite linear;
} }
.left-right { .left-right {
transform-origin: 0% 50%; transform-origin: 0% 50%;
} }
@keyframes progress {
@keyframes progress {
0% { 0% {
transform: translateX(0) scaleX(0); transform: translateX(0) scaleX(0);
} }
40% { 40% {
transform: translateX(0) scaleX(0.4); transform: translateX(0) scaleX(0.4);
} }
100% { 100% {
transform: translateX(100%) scaleX(0.5); transform: translateX(100%) scaleX(0.5);
} }
} }