From 34b6aab6b7c83daad5d9de005b3067d27a6e3c7d Mon Sep 17 00:00:00 2001 From: Araozu Date: Sat, 7 Oct 2023 11:04:20 -0500 Subject: [PATCH] [Classroom] Show & update user's account expiration date --- backend/.gitignore | 1 + backend/src/controller/classroom/mod.rs | 38 +++ backend/src/main.rs | 3 + .../online_classroom/get_expiration_date.rs | 23 ++ backend/src/online_classroom/mod.rs | 2 + backend/src/online_classroom/session.rs | 51 +++- .../update_expiration_date.rs | 274 ++++++++++++++++++ .../OnlineClassroom/ClassroomRegistration.tsx | 5 +- .../ClassroomUserCourses/index.tsx | 22 -- .../ClassroomUserInfo/AccountExpiration.tsx | 115 ++++++++ .../Courses.tsx | 2 +- .../Message.tsx | 10 +- .../ClassroomUserInfo/index.tsx | 27 ++ frontend/src/OnlineClassroom/index.tsx | 4 +- frontend/src/icons/ArrowsClockwiseIcon.tsx | 15 + 15 files changed, 553 insertions(+), 39 deletions(-) create mode 100644 backend/src/online_classroom/get_expiration_date.rs create mode 100644 backend/src/online_classroom/update_expiration_date.rs delete mode 100644 frontend/src/OnlineClassroom/ClassroomUserCourses/index.tsx create mode 100644 frontend/src/OnlineClassroom/ClassroomUserInfo/AccountExpiration.tsx rename frontend/src/OnlineClassroom/{ClassroomUserCourses => ClassroomUserInfo}/Courses.tsx (98%) rename frontend/src/OnlineClassroom/{ClassroomUserCourses => ClassroomUserInfo}/Message.tsx (91%) create mode 100644 frontend/src/OnlineClassroom/ClassroomUserInfo/index.tsx create mode 100644 frontend/src/icons/ArrowsClockwiseIcon.tsx diff --git a/backend/.gitignore b/backend/.gitignore index 4197690..ace76ec 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -2,3 +2,4 @@ target .env aulavirtual scraps +request-logs diff --git a/backend/src/controller/classroom/mod.rs b/backend/src/controller/classroom/mod.rs index b5b0f31..a19e316 100644 --- a/backend/src/controller/classroom/mod.rs +++ b/backend/src/controller/classroom/mod.rs @@ -1,4 +1,5 @@ use crate::online_classroom::register_course::register_course; +use crate::online_classroom::update_expiration_date::update_expiration_date; use crate::{ json_result::JsonResult, model::{ @@ -47,3 +48,40 @@ pub async fn register_course_contr( Err(err) => return (Status::InternalServerError, JsonResult::err(err)), } } + +#[get("/classroom/expiration_date/")] +pub async fn get_expiration_date(user_id: i32) -> (Status, Json>) { + match crate::online_classroom::get_expiration_date::get_expiration_date(user_id).await { + Ok(date) => return (Status::Ok, JsonResult::ok(date)), + Err(err) => return (Status::InternalServerError, JsonResult::err(err)), + } +} + +// +// Set expiration date +// + +#[derive(Debug, serde::Deserialize)] +pub struct ExpirationDate { + pub date: String, +} + +#[options("/classroom/expiration_date/<_u>")] +pub async fn set_expiration_date_options(_u: i32) -> Status { + Status::Ok +} + +#[put( + "/classroom/expiration_date/", + format = "json", + data = "" +)] +pub async fn set_expiration_date( + user_id: i32, + data: Json, +) -> (Status, Json>) { + match update_expiration_date(user_id, data.date.clone()).await { + Ok(()) => return (Status::Ok, JsonResult::ok(())), + Err(err) => return (Status::InternalServerError, JsonResult::err(err)), + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index cf1efd2..bae1bfa 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -64,6 +64,9 @@ async fn rocket() -> _ { controller::classroom::get_courses, controller::classroom::register_course_contr_options, controller::classroom::register_course_contr, + controller::classroom::get_expiration_date, + controller::classroom::set_expiration_date_options, + controller::classroom::set_expiration_date, ], ) } diff --git a/backend/src/online_classroom/get_expiration_date.rs b/backend/src/online_classroom/get_expiration_date.rs new file mode 100644 index 0000000..830686a --- /dev/null +++ b/backend/src/online_classroom/get_expiration_date.rs @@ -0,0 +1,23 @@ +use scraper::{Html, Selector}; + +use super::session::request; + +/// Gets the expiration date of the user with the given id, in format "YYYY-MM-DD HH:MM" +pub async fn get_expiration_date(user_id: i32) -> Result { + let html = request(format!("/main/admin/user_edit.php?user_id={}", user_id)).await?; + + let fragment = Html::parse_document(&html); + + let date_selector = Selector::parse("#expiration_date_alt") + .or_else(|err| Err(format!("Error creating date selector: {:?}", err)))?; + + let input_el = match fragment.select(&date_selector).next() { + Some(el) => el, + None => return Err(format!("#expiration_date_alt not found")), + }; + + match input_el.value().attr("value") { + Some(date) => Ok(String::from(date)), + None => return Err(format!("value attribute not found in #expiration_date_alt")), + } +} diff --git a/backend/src/online_classroom/mod.rs b/backend/src/online_classroom/mod.rs index 30f8b8f..047b1b0 100644 --- a/backend/src/online_classroom/mod.rs +++ b/backend/src/online_classroom/mod.rs @@ -6,8 +6,10 @@ use self::session::ensure_session; pub mod create_user; pub mod get_courses; +pub mod get_expiration_date; pub mod register_course; mod session; +pub mod update_expiration_date; pub mod user; /// Tries to connect to the online classroom, and get a session cookie diff --git a/backend/src/online_classroom/session.rs b/backend/src/online_classroom/session.rs index f1b0e74..104ad1c 100644 --- a/backend/src/online_classroom/session.rs +++ b/backend/src/online_classroom/session.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Local, TimeZone, Utc}; +use chrono::{DateTime, Local}; use lazy_static::lazy_static; use std::time::{SystemTime, UNIX_EPOCH}; use urlencoding::encode; @@ -17,6 +17,47 @@ lazy_static! { static ref SESSION_TIME: RwLock = RwLock::new(0); } +/// Make a post request with a body and content type +pub async fn classroom_post_redirect( + url: String, + content_type: String, + body: String, +) -> Result<(), String> { + let classroom_url = std::env::var("CLASSROOM_URL").expect("CLASSROOM_URL env var is not set!"); + + ensure_session().await?; + + // Get the stored client + let jar = SESSION_COOKIE.read().unwrap().jar.clone(); + + let uri = format!("{}{}", classroom_url, url); + + // Do the request + let response = Request::post(uri) + .header("Content-Type", content_type) + .cookie_jar(jar) + .body(body) + .or_else(|err| Err(format!("Error creating request: {:?}", err)))? + .send(); + + let mut response = match response { + Ok(r) => r, + Err(err) => return Err(format!("Error sending request: {:?}", err)), + }; + + if response.status() == isahc::http::StatusCode::FOUND { + return Ok(()); + } + + match response.text() { + Ok(t) => { + log_html(&t); + Err(format!("Unexpected response from classroom.")) + } + Err(err) => Err(format!("Error getting text from response: {:?}", err)), + } +} + /// Makes a request to the online classroom, and returns the html string pub async fn request(url: String) -> Result { let classroom_url = std::env::var("CLASSROOM_URL").expect("CLASSROOM_URL env var is not set!"); @@ -119,7 +160,7 @@ pub async fn register_courses_request(url: String, body: String) -> Result { log_html(&t); Ok(t) - }, + } Err(err) => Err(format!("Error getting text from response: {:?}", err)), } } @@ -159,8 +200,8 @@ async fn login() -> Result<(), String> { let login_body = format!( "login={}&password={}&submitAuth=&_qf__formLogin=", - encode(classroom_user.as_str()).into_owned(), - encode(classroom_password.as_str()).into_owned(), + encode(classroom_user.as_str()).into_owned(), + encode(classroom_password.as_str()).into_owned(), ); let response = Request::post(format!("{}/index.php", classroom_url)) @@ -195,7 +236,7 @@ async fn login() -> Result<(), String> { } } -fn log_html(html: &String) { +pub fn log_html(html: &String) { // Get current time and date in iso let now: DateTime = Local::now(); let now = now.to_rfc3339(); diff --git a/backend/src/online_classroom/update_expiration_date.rs b/backend/src/online_classroom/update_expiration_date.rs new file mode 100644 index 0000000..9792d18 --- /dev/null +++ b/backend/src/online_classroom/update_expiration_date.rs @@ -0,0 +1,274 @@ +use scraper::{Html, Selector}; + +use super::session::{classroom_post_redirect, request}; + +pub async fn update_expiration_date( + user_id: i32, + new_expiration_date: String, +) -> Result<(), String> { + let html = request(format!("/main/admin/user_edit.php?user_id={}", user_id)).await?; + + let req_body = { + let html = Html::parse_document(&html); + + let surnames_selector = + Selector::parse("#user_edit_lastname").expect("Error creating surnames selector"); + let names_selector = + Selector::parse("#user_edit_firstname").expect("Error creating surnames selector"); + let email_selector = + Selector::parse("#user_edit_email").expect("Error creating surnames selector"); + let username_selector = + Selector::parse("#user_edit_username").expect("Error creating surnames selector"); + let protect_token_sel = Selector::parse("#user_edit_protect_token") + .expect("Error creating protect_token selector"); + + let surnames = html + .select(&surnames_selector) + .next() + .ok_or(format!("surnames element not found"))? + .value() + .attr("value") + .ok_or(format!("surnames input has no value attribute"))?; + + let names = html + .select(&names_selector) + .next() + .ok_or(format!("names element not found"))? + .value() + .attr("value") + .ok_or(format!("names input has no value attribute"))?; + + let email = html + .select(&email_selector) + .next() + .ok_or(format!("email element not found"))? + .value() + .attr("value") + .ok_or(format!("email input has no value attribute"))?; + + let username = html + .select(&username_selector) + .next() + .ok_or(format!("username element not found"))? + .value() + .attr("value") + .ok_or(format!("username input has no value attribute"))?; + + let protect_token = html + .select(&protect_token_sel) + .next() + .ok_or(format!("protect_token element not found"))? + .value() + .attr("value") + .ok_or(format!("protect_token input has no value attribute"))?; + + get_body( + surnames.into(), + names.into(), + email.into(), + username.into(), + new_expiration_date, + protect_token.into(), + ) + }; + + classroom_post_redirect( + "/main/admin/user_edit.php".into(), + "multipart/form-data; boundary=---------------------------318235432819784070062970146417" + .into(), + req_body, + ) + .await +} + +fn get_body( + surnames: String, + names: String, + email: String, + username: String, + new_expiration_date: String, + protect_token: String, +) -> String { + format!( + r#" +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="lastname" + +{surnames} +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="firstname" + +{names} +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="official_code" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="email" + +{email} +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="phone" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="picture"; filename="" +Content-Type: application/octet-stream + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="username" + +{username} +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="reset_password" + +0 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="password" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="status" + +5 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="platform_admin" + +0 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="language" + +spanish +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="send_mail" + +0 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="radio_expiration_date" + +1 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="expiration_date" + +{new_expiration_date} 23:00 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="active" + +1 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="q" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="q" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_legal_accept" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_already_logged_in" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_update_type" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_rssfeeds" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_dashboard" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_timezone" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_mail_notify_invitation" + +1 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_mail_notify_message" + +1 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_mail_notify_group_message" + +1 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_user_chat_status" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_google_calendar_url" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_captcha_blocked_until_date" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_skype" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_linkedin_url" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_request_for_legal_agreement_consent_removal_justification" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_request_for_delete_account_justification" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_request_for_legal_agreement_consent_removal" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="extra_request_for_delete_account" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="submit" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="_qf__user_edit" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="protect_token" + +{protect_token} +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="user_id" + +2140 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="MAX_FILE_SIZE" + +536870912 +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="picture_crop_result" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="picture_crop_image_base_64" + + +-----------------------------318235432819784070062970146417 +Content-Disposition: form-data; name="item_id" + +2140 +-----------------------------318235432819784070062970146417-- + "# + ) +} diff --git a/frontend/src/OnlineClassroom/ClassroomRegistration.tsx b/frontend/src/OnlineClassroom/ClassroomRegistration.tsx index d8fa8aa..08148a4 100644 --- a/frontend/src/OnlineClassroom/ClassroomRegistration.tsx +++ b/frontend/src/OnlineClassroom/ClassroomRegistration.tsx @@ -15,7 +15,7 @@ export function ClassroomRegistration(props: {

Inscribir en Aula Virtual

-
+
setSelections((s) => new Set([...s, x]))} /> @@ -45,9 +45,6 @@ export function ClassroomRegistration(props: { function ManualClassroomRegistration(props: {onAdd: (k: ClassroomCourseValue) => void}) { return (
-

Haz click en un curso para agregarlo

-
- diff --git a/frontend/src/OnlineClassroom/ClassroomUserCourses/index.tsx b/frontend/src/OnlineClassroom/ClassroomUserCourses/index.tsx deleted file mode 100644 index b507a28..0000000 --- a/frontend/src/OnlineClassroom/ClassroomUserCourses/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createSignal } from "solid-js"; -import { ClassroomCourse } from "../../types/ClassroomCourse"; -import { Person } from "../../types/Person"; -import { CoursesList } from "./Courses"; -import { Message } from "./Message"; - -export function ClassroomUserCourses(props: {userid: number, updateSignal: number, person: Person}) { - const [courses, setCourses] = createSignal>([]); - - return ( -
- - - -
- ); -} diff --git a/frontend/src/OnlineClassroom/ClassroomUserInfo/AccountExpiration.tsx b/frontend/src/OnlineClassroom/ClassroomUserInfo/AccountExpiration.tsx new file mode 100644 index 0000000..6a82f7f --- /dev/null +++ b/frontend/src/OnlineClassroom/ClassroomUserInfo/AccountExpiration.tsx @@ -0,0 +1,115 @@ +import { createMemo, createSignal, onMount } from "solid-js"; +import { OutlinedCard } from "../../components/OutlinedCard"; +import { LoadingStatus, backend, useLoading, wait } from "../../utils/functions"; +import { LoadingIcon } from "../../icons/LoadingIcon"; +import { ArrowsClockwiseIcon } from "../../icons/ArrowsClockwiseIcon"; +import { JsonResult } from "../../types/JsonResult"; + +export function AccountExpiration(props: {userId: number}) { + const [expirationDate, setExpirationDate] = createSignal(null); // YYYY-MM-DD + const {status, setStatus} = useLoading(); + + const loading = createMemo(() => status() === LoadingStatus.Loading); + + const loadExpiration = async() => { + setStatus(LoadingStatus.Loading); + setExpirationDate(null); + if (import.meta.env.DEV) await wait(1500); + + backend.get>(`/api/classroom/expiration_date/${props.userId}`) + .then((response) => { + if (response.status === 200) { + const date = response.data.Ok.substring(0, 10); + console.log(date); + setExpirationDate(date); + setStatus(LoadingStatus.Ok); + } + }) + .catch((err) => { + console.log(err); + + setStatus(LoadingStatus.Error); + }); + }; + + const setExpiration = async() => { + setStatus(LoadingStatus.Loading); + + if (import.meta.env.DEV) await wait(1500); + + backend.put>( + `/api/classroom/expiration_date/${props.userId}`, + {date: expirationDate()}, + ) + .then((response) => { + if (response.status === 200) { + setStatus(LoadingStatus.Ok); + } + }) + .catch((error) => { + console.error(error); + setStatus(LoadingStatus.Error); + }); + }; + + onMount(loadExpiration); + + return ( + +

+ Fecha de expiración del acceso +

+ +
+

+ + Fecha de expiración: + + + setExpirationDate(e.target.value)} + disabled={status() === LoadingStatus.Loading} + /> +

+ +
+ +
+
+ +
+ ); +} diff --git a/frontend/src/OnlineClassroom/ClassroomUserCourses/Courses.tsx b/frontend/src/OnlineClassroom/ClassroomUserInfo/Courses.tsx similarity index 98% rename from frontend/src/OnlineClassroom/ClassroomUserCourses/Courses.tsx rename to frontend/src/OnlineClassroom/ClassroomUserInfo/Courses.tsx index 48e6ada..2c437a3 100644 --- a/frontend/src/OnlineClassroom/ClassroomUserCourses/Courses.tsx +++ b/frontend/src/OnlineClassroom/ClassroomUserInfo/Courses.tsx @@ -41,7 +41,7 @@ export function CoursesList(props: {userid: number, updateSignal: number, course }); return ( - +

Cursos matriculados

diff --git a/frontend/src/OnlineClassroom/ClassroomUserCourses/Message.tsx b/frontend/src/OnlineClassroom/ClassroomUserInfo/Message.tsx similarity index 91% rename from frontend/src/OnlineClassroom/ClassroomUserCourses/Message.tsx rename to frontend/src/OnlineClassroom/ClassroomUserInfo/Message.tsx index 7b3b13f..7c9ce2f 100644 --- a/frontend/src/OnlineClassroom/ClassroomUserCourses/Message.tsx +++ b/frontend/src/OnlineClassroom/ClassroomUserInfo/Message.tsx @@ -30,15 +30,15 @@ Correo electrónico: soporte@eegsac.com const companyMessage = ` Buen día estimado, -Se adjunta la lista de (los) colaboradore(s) y su(s) acceso(s) en la plataforma virtual. +Se adjunta usuario, contraseña y cursos de la plataforma virtual para el personal solicitado. El vínculo para acceder al aula virtual es el siguiente: https://aulavirtual.eegsac.com/ . Quedo atenta cualquier duda o consulta. -Saludos Cordiales! +Saludos Cordiales. `.trim(); return ( - +

Generar mensajes

@@ -60,7 +60,7 @@ Saludos Cordiales!
                         {personMessage()}
                     
@@ -82,7 +82,7 @@ Saludos Cordiales!
                         {companyMessage}
                     
diff --git a/frontend/src/OnlineClassroom/ClassroomUserInfo/index.tsx b/frontend/src/OnlineClassroom/ClassroomUserInfo/index.tsx new file mode 100644 index 0000000..e1b7058 --- /dev/null +++ b/frontend/src/OnlineClassroom/ClassroomUserInfo/index.tsx @@ -0,0 +1,27 @@ +import { createSignal } from "solid-js"; +import { ClassroomCourse } from "../../types/ClassroomCourse"; +import { Person } from "../../types/Person"; +import { CoursesList } from "./Courses"; +import { Message } from "./Message"; +import { AccountExpiration } from "./AccountExpiration"; + +export function ClassroomUserInfo(props: {userid: number, updateSignal: number, person: Person}) { + const [courses, setCourses] = createSignal>([]); + + return ( +
+
+ + + +
+ + +
+ ); +} diff --git a/frontend/src/OnlineClassroom/index.tsx b/frontend/src/OnlineClassroom/index.tsx index 4795774..3936328 100644 --- a/frontend/src/OnlineClassroom/index.tsx +++ b/frontend/src/OnlineClassroom/index.tsx @@ -4,7 +4,7 @@ import { Person } from "../types/Person"; import { FilledCard } from "../components/FilledCard"; import { ClassroomUserCreation } from "./ClassroomUserCreation"; import { ClassroomVinculation } from "./ClassroomVinculation"; -import { ClassroomUserCourses } from "./ClassroomUserCourses"; +import { ClassroomUserInfo } from "./ClassroomUserInfo"; import { ClassroomRegistration } from "./ClassroomRegistration"; type TabType = "Vinculate" | "Create"; @@ -40,7 +40,7 @@ export function OnlineClassroom() { onSuccess={() => setUpdateSIgnal((s) => s + 1)} /> - + + + ); +}