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}"
}
steps {
// Clean workspace. TODO: Separate backend & frontend build
cleanWs()
checkout scm
dir("backend") {
sh "touch .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
request-logs
.idea
.directory

48
backend/Cargo.lock generated
View File

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

View File

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
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" ] }
dotenvy = "0.15.7"
serde = "1.0.188"
@ -16,7 +16,6 @@ scraper = "0.17.1"
isahc = { version = "1.7.2", features = ["cookies"] }
urlencoding = "2.1.3"
lazy_static = "1.4.0"
once_cell = "1.18.0"
log = "0.4.20"
env_logger = "0.10.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;
pub use courses::disenroll_user_options;
pub use courses::disenroll_user;
pub use courses::disenroll_user_options;
#[options("/classroom/user")]
pub fn create_user_options() -> Status {

View File

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

View File

@ -1,8 +1,6 @@
use cors::Cors;
use once_cell::sync::OnceCell;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::MySql;
use sqlx::Pool;
use sqlx::Connection;
use sqlx::MySqlConnection;
use std::env;
use std::time::Instant;
@ -16,17 +14,15 @@ mod online_classroom;
pub mod json_result;
static DB: OnceCell<Pool<MySql>> = OnceCell::new();
/// Opens & returns a connection to the database
///
/// We don't use a connection pool because it often times out,
/// we don't have a lot of traffic and reiniting the pool would
/// require to change a lot of code in multiple places.
pub async fn db() -> Result<&'static Pool<MySql>, String> {
/// We don't use a connection pool because on some days, on the afternoon,
/// the connections die and the user has to wait for all 5 connections
/// in the pool to die and then wait for the new connections to be created.
pub async fn db() -> Result<MySqlConnection, 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()),
@ -43,73 +39,6 @@ pub async fn db() -> Result<&'static Pool<MySql>, 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]

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 {
Ok(res) => res,
@ -71,7 +73,7 @@ impl Course {
"SELECT course_name FROM course WHERE course_id = ?",
course_id
)
.fetch_one(db)
.fetch_one(&mut db)
.await;
match res {

View File

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

View File

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

View File

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

View File

@ -103,6 +103,9 @@ pub async fn request(url: String) -> Result<String, String> {
// Get the stored client
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);
// Do the request
@ -252,11 +255,23 @@ async fn login() -> Result<(), String> {
match response {
Ok(mut r) => {
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
SESSION_COOKIE.write().unwrap().jar = jar.clone();
Ok(())
} else {
log::info!("classroom login: not 302");
// Write html to file
match r.text() {
Ok(t) => {

3
frontend/.gitignore vendored
View File

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

View File

@ -3,7 +3,7 @@
--c-on-error: #690005;
--c-error-container: #93000a;
--c-on-error-container: #ffdad6;
--c-outline-50: rgba(143, 144, 154, 0.5);
}
@ -32,20 +32,22 @@ body {
.progress {
animation: progress 1s infinite linear;
}
.left-right {
transform-origin: 0% 50%;
}
@keyframes progress {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
}
.left-right {
transform-origin: 0% 50%;
}
@keyframes progress {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}