[Classroom] Show & update user's account expiration date

This commit is contained in:
Araozu 2023-10-07 11:04:20 -05:00
parent d30a75697b
commit 34b6aab6b7
15 changed files with 553 additions and 39 deletions

1
backend/.gitignore vendored
View File

@ -2,3 +2,4 @@ target
.env .env
aulavirtual aulavirtual
scraps scraps
request-logs

View File

@ -1,4 +1,5 @@
use crate::online_classroom::register_course::register_course; use crate::online_classroom::register_course::register_course;
use crate::online_classroom::update_expiration_date::update_expiration_date;
use crate::{ use crate::{
json_result::JsonResult, json_result::JsonResult,
model::{ model::{
@ -47,3 +48,40 @@ pub async fn register_course_contr(
Err(err) => return (Status::InternalServerError, JsonResult::err(err)), Err(err) => return (Status::InternalServerError, JsonResult::err(err)),
} }
} }
#[get("/classroom/expiration_date/<user_id>")]
pub async fn get_expiration_date(user_id: i32) -> (Status, Json<JsonResult<String>>) {
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/<user_id>",
format = "json",
data = "<data>"
)]
pub async fn set_expiration_date(
user_id: i32,
data: Json<ExpirationDate>,
) -> (Status, Json<JsonResult<()>>) {
match update_expiration_date(user_id, data.date.clone()).await {
Ok(()) => return (Status::Ok, JsonResult::ok(())),
Err(err) => return (Status::InternalServerError, JsonResult::err(err)),
}
}

View File

@ -64,6 +64,9 @@ async fn rocket() -> _ {
controller::classroom::get_courses, controller::classroom::get_courses,
controller::classroom::register_course_contr_options, controller::classroom::register_course_contr_options,
controller::classroom::register_course_contr, controller::classroom::register_course_contr,
controller::classroom::get_expiration_date,
controller::classroom::set_expiration_date_options,
controller::classroom::set_expiration_date,
], ],
) )
} }

View File

@ -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<String, String> {
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")),
}
}

View File

@ -6,8 +6,10 @@ use self::session::ensure_session;
pub mod create_user; pub mod create_user;
pub mod get_courses; pub mod get_courses;
pub mod get_expiration_date;
pub mod register_course; pub mod register_course;
mod session; mod session;
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 get a session cookie

View File

@ -1,4 +1,4 @@
use chrono::{DateTime, Local, TimeZone, Utc}; use chrono::{DateTime, Local};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use urlencoding::encode; use urlencoding::encode;
@ -17,6 +17,47 @@ lazy_static! {
static ref SESSION_TIME: RwLock<u64> = RwLock::new(0); static ref SESSION_TIME: RwLock<u64> = 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 /// Makes a request to the online classroom, and returns the html string
pub async fn request(url: String) -> Result<String, String> { pub async fn request(url: String) -> Result<String, String> {
let classroom_url = std::env::var("CLASSROOM_URL").expect("CLASSROOM_URL env var is not set!"); 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<Strin
Ok(t) => { Ok(t) => {
log_html(&t); log_html(&t);
Ok(t) Ok(t)
}, }
Err(err) => Err(format!("Error getting text from response: {:?}", err)), Err(err) => Err(format!("Error getting text from response: {:?}", err)),
} }
} }
@ -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 // Get current time and date in iso
let now: DateTime<Local> = Local::now(); let now: DateTime<Local> = Local::now();
let now = now.to_rfc3339(); let now = now.to_rfc3339();

View File

@ -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--
"#
)
}

View File

@ -15,7 +15,7 @@ export function ClassroomRegistration(props: {
<FilledCard class="border border-c-outline overflow-hidden"> <FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-3 font-bold text-xl">Inscribir en Aula Virtual</h2> <h2 class="p-3 font-bold text-xl">Inscribir en Aula Virtual</h2>
<div class="bg-c-surface p-4 h-[23rem]"> <div class="bg-c-surface p-4 h-[17rem]">
<ManualClassroomRegistration <ManualClassroomRegistration
onAdd={(x) => setSelections((s) => new Set([...s, x]))} onAdd={(x) => setSelections((s) => new Set([...s, x]))}
/> />
@ -45,9 +45,6 @@ export function ClassroomRegistration(props: {
function ManualClassroomRegistration(props: {onAdd: (k: ClassroomCourseValue) => void}) { function ManualClassroomRegistration(props: {onAdd: (k: ClassroomCourseValue) => void}) {
return ( return (
<form> <form>
<p>Haz click en un curso para agregarlo</p>
<br />
<ClassroomSearchableSelect <ClassroomSearchableSelect
onAdd={props.onAdd} onAdd={props.onAdd}
/> />

View File

@ -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<Array<ClassroomCourse>>([]);
return (
<div class="flex gap-2 flex-wrap">
<CoursesList
userid={props.userid}
updateSignal={props.updateSignal}
courses={courses()}
setCourses={setCourses}
/>
<Message courses={courses()} person={props.person} />
</div>
);
}

View File

@ -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<string | null>(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<JsonResult<string>>(`/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<JsonResult<null>>(
`/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 (
<OutlinedCard class="w-[24rem] h-[10.5rem] overflow-hidden">
<h2 class="text-xl p-3 bg-c-surface-variant text-c-on-surface-variant">
Fecha de expiración del acceso
</h2>
<div class="px-4 py-4">
<p class="grid grid-cols-[auto_9rem] items-center">
<span>
Fecha de expiración:
</span>
<input
class="bg-c-surface text-c-on-surface border border-c-outline rounded-lg py-1 px-2 font-mono
disabled:opacity-50 disabled:cursor-not-allowed"
type="date"
name="classroom-expiration-update"
id="classroom-expiration-update"
value={expirationDate() ?? ""}
oninput={(e) => setExpirationDate(e.target.value)}
disabled={status() === LoadingStatus.Loading}
/>
</p>
<div class="text-right pt-2">
<button
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed relative"
type="button"
disabled={loading()}
onclick={setExpiration}
>
<span class="mr-6">
Actualizar expiracion
</span>
<span
class="absolute top-1 right-2"
style={{display: loading() ? "inline-block" : "none"}}
>
<LoadingIcon
class="animate-spin"
fill="var(--c-primary-container)"
/>
</span>
<span
class="absolute top-1 right-2"
style={{display: loading() ? "none" : "inline-block"}}
>
<ArrowsClockwiseIcon
fill="var(--c-on-primary)"
/>
</span>
</button>
</div>
</div>
</OutlinedCard>
);
}

View File

@ -41,7 +41,7 @@ export function CoursesList(props: {userid: number, updateSignal: number, course
}); });
return ( return (
<OutlinedCard class="w-[24rem] h-[26.5rem] overflow-hidden"> <OutlinedCard class="w-[24rem] h-[19.5rem] overflow-hidden">
<h2 class="text-xl p-3 bg-c-surface-variant text-c-on-surface-variant"> <h2 class="text-xl p-3 bg-c-surface-variant text-c-on-surface-variant">
Cursos matriculados Cursos matriculados
</h2> </h2>

View File

@ -30,15 +30,15 @@ Correo electrónico: soporte@eegsac.com
const companyMessage = ` const companyMessage = `
Buen día estimado, 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/ . El vínculo para acceder al aula virtual es el siguiente: https://aulavirtual.eegsac.com/ .
Quedo atenta cualquier duda o consulta. Quedo atenta cualquier duda o consulta.
Saludos Cordiales! Saludos Cordiales.
`.trim(); `.trim();
return ( return (
<OutlinedCard class="w-[24rem] h-[26.5rem] overflow-hidden"> <OutlinedCard class="w-[24rem] h-[30.5rem] overflow-hidden">
<h2 class="text-xl p-3 bg-c-surface-variant text-c-on-surface-variant"> <h2 class="text-xl p-3 bg-c-surface-variant text-c-on-surface-variant">
Generar mensajes Generar mensajes
</h2> </h2>
@ -60,7 +60,7 @@ Saludos Cordiales!
<pre <pre
class="w-full p-2 bg-c-surface text-c-on-surface class="w-full p-2 bg-c-surface text-c-on-surface
border border-c-outline rounded font-mono text-sm border border-c-outline rounded font-mono text-sm
whitespace-pre-wrap max-h-32 overflow-x-scroll shadow-md" whitespace-pre-wrap max-h-40 overflow-x-scroll shadow-md"
> >
{personMessage()} {personMessage()}
</pre> </pre>
@ -82,7 +82,7 @@ Saludos Cordiales!
<pre <pre
class="w-full p-2 bg-c-surface text-c-on-surface class="w-full p-2 bg-c-surface text-c-on-surface
border border-c-outline rounded font-mono text-sm border border-c-outline rounded font-mono text-sm
whitespace-pre-wrap max-h-28 overflow-x-scroll shadow-md" whitespace-pre-wrap max-h-36 overflow-x-scroll shadow-md"
> >
{companyMessage} {companyMessage}
</pre> </pre>

View File

@ -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<Array<ClassroomCourse>>([]);
return (
<div class="flex gap-2 flex-wrap">
<div>
<CoursesList
userid={props.userid}
updateSignal={props.updateSignal}
courses={courses()}
setCourses={setCourses}
/>
<AccountExpiration userId={props.userid} />
</div>
<Message courses={courses()} person={props.person} />
</div>
);
}

View File

@ -4,7 +4,7 @@ import { Person } from "../types/Person";
import { FilledCard } from "../components/FilledCard"; import { FilledCard } from "../components/FilledCard";
import { ClassroomUserCreation } from "./ClassroomUserCreation"; import { ClassroomUserCreation } from "./ClassroomUserCreation";
import { ClassroomVinculation } from "./ClassroomVinculation"; import { ClassroomVinculation } from "./ClassroomVinculation";
import { ClassroomUserCourses } from "./ClassroomUserCourses"; import { ClassroomUserInfo } from "./ClassroomUserInfo";
import { ClassroomRegistration } from "./ClassroomRegistration"; import { ClassroomRegistration } from "./ClassroomRegistration";
type TabType = "Vinculate" | "Create"; type TabType = "Vinculate" | "Create";
@ -40,7 +40,7 @@ export function OnlineClassroom() {
onSuccess={() => setUpdateSIgnal((s) => s + 1)} onSuccess={() => setUpdateSIgnal((s) => s + 1)}
/> />
<ClassroomUserCourses <ClassroomUserInfo
userid={person()!.person_classroom_id!} userid={person()!.person_classroom_id!}
updateSignal={updateSignal()} updateSignal={updateSignal()}
person={person()!} person={person()!}

View File

@ -0,0 +1,15 @@
export function ArrowsClockwiseIcon(props: {fill: string, size?: number, class?: string}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
class={`inline-block w-6 ${props.class}`}
width={props.size ?? 32}
height={props.size ?? 32}
fill={props.fill}
viewBox="0 0 256 256"
>
<path d="M197.67,186.37a8,8,0,0,1,0,11.29C196.58,198.73,170.82,224,128,224c-37.39,0-64.53-22.4-80-39.85V208a8,8,0,0,1-16,0V160a8,8,0,0,1,8-8H88a8,8,0,0,1,0,16H55.44C67.76,183.35,93,208,128,208c36,0,58.14-21.46,58.36-21.68A8,8,0,0,1,197.67,186.37ZM216,40a8,8,0,0,0-8,8V71.85C192.53,54.4,165.39,32,128,32,85.18,32,59.42,57.27,58.34,58.34a8,8,0,0,0,11.3,11.34C69.86,69.46,92,48,128,48c35,0,60.24,24.65,72.56,40H168a8,8,0,0,0,0,16h48a8,8,0,0,0,8-8V48A8,8,0,0,0,216,40Z" />
</svg>
);
}