diff --git a/src/main.rs b/src/main.rs index f166174ed6e71ad5a4a924abbc5e45ae278d6e79..4f4da2ad19517d51ee3016ae9a6135ce3b2a418f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,17 +35,15 @@ mod broadcaster; mod import_contest; mod language; mod models; +mod pages; mod queue; mod schema; mod setup; -mod pages; - -type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; async fn update_database( mut job_result_receiver: broadcast::Receiver<JobResult>, - pool: DbPool, -) -> Result<(), pages::PageError> { + pool: pages::prelude::DbPool, +) -> Result<(), pages::prelude::PageError> { loop { let job_result = job_result_receiver.recv().await.unwrap(); if let JobResult { @@ -196,12 +194,12 @@ async fn main() -> Result<(), Box<dyn Error>> { .app_data(handlebars_ref.clone()) .service( web::scope("/jughisto") - .service(pages::get_login) + .service(pages::get_login::get_login) .service(pages::get_me) .service(pages::change_password) .service(pages::post_login) .service(pages::post_logout) - .service(pages::get_main) + .service(pages::get_main::get_main) .service(pages::get_contests) .service(pages::get_contest_by_id) .service(pages::get_contest_scoreboard_by_id) diff --git a/src/pages/get_login.rs b/src/pages/get_login.rs new file mode 100644 index 0000000000000000000000000000000000000000..10515464416fb9a2a32e3bc2e85c6bffbee93762 --- /dev/null +++ b/src/pages/get_login.rs @@ -0,0 +1,10 @@ +use crate::pages::prelude::*; + +#[get("/login")] +pub async fn get_login(base: BaseContext, hb: Data<Handlebars<'_>>) -> PageResult { + #[derive(Serialize)] + struct Context { + base: BaseContext, + } + render(&hb, "login", &Context { base }) +} diff --git a/src/pages/get_main.rs b/src/pages/get_main.rs new file mode 100644 index 0000000000000000000000000000000000000000..2fdc5e839a4ac2ca6c4d409c5ed5b18819a80fe3 --- /dev/null +++ b/src/pages/get_main.rs @@ -0,0 +1,38 @@ +use chrono_tz::Tz; + +use crate::models::submission; +use crate::pages::prelude::*; +use crate::pages::{ + get_formatted_contests, get_formatted_submissions, FormattedContest, FormattedSubmission, +}; + +#[get("/")] +async fn get_main( + base: BaseContext, + identity: Option<Identity>, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = get_identity(&identity.as_ref()); + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contests: Vec<FormattedContest>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let submissions = submission::get_submissions(&mut connection)?; + + render( + &hb, + "main", + &Context { + base, + contests: get_formatted_contests(&mut connection, logged_user.map(|u| u.id), &tz)?, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index cff4464660a6e6856d3f5cc665fd4e467dde1aa6..3ffd63d4076e2b737d5ca04e1751cd8be6156a51 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,11 +1,9 @@ use std::collections::HashMap; use std::convert::TryFrom; use std::fs::{create_dir_all, File}; -use std::future::Future; use std::io::{Cursor, Read, Write}; use std::iter::FromIterator; use std::path::PathBuf; -use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::{env, fs, str}; @@ -13,12 +11,11 @@ use actix_files::NamedFile; use actix_identity::Identity; use actix_multipart::Multipart; use actix_session::Session; -use actix_web::dev::Payload; -use actix_web::http::header::{ContentType, HeaderValue}; +use actix_web::http::header::HeaderValue; use actix_web::http::{header, StatusCode}; use actix_web::middleware::ErrorHandlerResponse; -use actix_web::{dev, get, post, web, FromRequest, HttpMessage, HttpRequest, HttpResponse}; -use actix_web_flash_messages::{FlashMessage, IncomingFlashMessages}; +use actix_web::{dev, get, post, web, HttpMessage, HttpRequest, HttpResponse}; +use actix_web_flash_messages::FlashMessage; use async_channel::Sender; use chrono::prelude::*; use chrono_tz::Tz; @@ -26,15 +23,14 @@ use contest::{Contest, ContestWithAcs}; use dashmap::DashMap; use diesel::pg::PgConnection; use futures::{StreamExt, TryStreamExt}; -use handlebars::{Handlebars, RenderError}; +use handlebars::Handlebars; use itertools::Itertools; use lazy_static::lazy_static; -use log::{error, info}; +use log::info; use problem::ProblemByContestWithScore; use regex::Regex; use serde::{Deserialize, Serialize}; use submission::{ContestProblem, Submission}; -use thiserror::Error; use tokio::sync::broadcast; use user::{PasswordMatched, User, UserHashingError}; use uuid::Uuid; @@ -43,118 +39,13 @@ use crate::broadcaster::Broadcaster; use crate::models::problem::ProblemByContest; use crate::models::{contest, problem, submission, user}; use crate::queue::job_protocol::{job, job_result, Job, JobResult, Language}; -use crate::{import_contest, language, DbPool}; - -#[derive(Error, Debug)] -pub enum PageError { - #[error("Unauthorized")] - Unauthorized(), - #[error("Couldn't render: {0}")] - Render(#[from] handlebars::RenderError), - #[error(transparent)] - SessionGet(#[from] actix_session::SessionGetError), - #[error("{0}")] - Custom(String), - #[error("{0}")] - Forbidden(String), - #[error("{0}")] - Validation(String), - #[error("Couldn't get connection from pool")] - ConnectionPool(#[from] r2d2::Error), - #[error("Couldn't hash")] - UserHashing(#[from] user::UserHashingError), - #[error(transparent)] - Web(#[from] actix_web::Error), - #[error(transparent)] - Queue(#[from] async_channel::SendError<Job>), - #[error("Couldn't fetch result from database")] - Database(#[from] diesel::result::Error), - #[error("couldn't insert session")] - SessionInsert(#[from] actix_session::SessionInsertError), - #[error("couldn't work with the filesystem")] - Io(#[from] std::io::Error), - #[error("couldn't work with the zip")] - Zip(#[from] zip::result::ZipError), -} - -fn error_response_and_log(me: &impl actix_web::error::ResponseError) -> HttpResponse { - error!("{}", me); - HttpResponse::build(me.status_code()) - .insert_header(ContentType::plaintext()) - .body(me.to_string()) -} - -impl actix_web::error::ResponseError for PageError { - fn error_response(&self) -> HttpResponse { - error_response_and_log(self) - } - - fn status_code(&self) -> StatusCode { - match *self { - PageError::Unauthorized() => StatusCode::UNAUTHORIZED, - PageError::Validation(_) => StatusCode::BAD_REQUEST, - PageError::Forbidden(_) => StatusCode::FORBIDDEN, - PageError::Custom(_) - | PageError::SessionInsert(_) - | PageError::ConnectionPool(_) - | PageError::Web(_) - | PageError::Render(_) - | PageError::Queue(_) - | PageError::SessionGet(_) - | PageError::Database(_) - | PageError::Io(_) - | PageError::UserHashing(_) - | PageError::Zip(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -type PageResult = Result<HttpResponse, PageError>; - -#[derive(Serialize)] -pub struct BaseContext { - logged_user: Option<LoggedUser>, - flash_messages: IncomingFlashMessages, - base_url: String, -} - -impl FromRequest for BaseContext { - type Error = actix_web::Error; - type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let req = req.clone(); - Box::pin(async move { - let identity = Option::<Identity>::from_request(&req, &mut Payload::None).await; - let logged_user = identity?.and_then(|identity| get_identity(&Some(&identity))); - let flash_messages = - IncomingFlashMessages::from_request(&req, &mut Payload::None).await?; - Ok(BaseContext { - logged_user, - flash_messages, - base_url: env::var("BASE_URL") - .expect("BASE_URL environment variable is not set"), - }) - }) - } -} +use crate::{import_contest, language}; -fn render<Err: From<RenderError>, Ctx: serde::Serialize>( - hb: &Handlebars, - name: &str, - context: &Ctx, -) -> Result<HttpResponse, Err> { - Ok(HttpResponse::Ok().body(hb.render(name, context)?)) -} +pub mod get_login; +pub mod get_main; +pub mod prelude; -#[get("/login")] -async fn get_login(base: BaseContext, hb: web::Data<Handlebars<'_>>) -> PageResult { - #[derive(Serialize)] - struct Context { - base: BaseContext, - } - render(&hb, "login", &Context { base }) -} +use prelude::*; #[get("/me")] async fn get_me( @@ -205,9 +96,7 @@ pub fn render_401<B>( Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } -pub fn render_400<B>( - res: dev::ServiceResponse<B>, -) -> actix_web::Result<ErrorHandlerResponse<B>> { +pub fn render_400<B>(res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> { FlashMessage::error("Entrada inválida").send(); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } @@ -218,19 +107,6 @@ struct LoginForm { password: String, } -fn get_identity(identity: &Option<&Identity>) -> Option<LoggedUser> { - identity.as_ref().and_then(|identity| { - let identity = identity.id(); - identity - .ok() - .and_then(|identity| serde_json::from_str(&identity).ok()) - }) -} - -fn require_identity(identity: &Identity) -> Result<LoggedUser, PageError> { - get_identity(&Some(identity)).ok_or(PageError::Unauthorized()) -} - fn format_duration(duration: chrono::Duration) -> String { format!( "{}{}{:02}:{:02}", @@ -274,11 +150,11 @@ fn get_formatted_problem_by_contest_with_score( None => "*".into(), }) .unwrap_or("".into()), - first_ac_submission_minutes: p.first_ac_submission_instant.and_then(|t| match contest - .start_instant - { - Some(cs) => Some((t - cs).num_minutes()), - None => None, + first_ac_submission_minutes: p.first_ac_submission_instant.and_then(|t| { + match contest.start_instant { + Some(cs) => Some((t - cs).num_minutes()), + None => None, + } }), failed_submissions: p.failed_submissions, id: p.id, @@ -319,11 +195,8 @@ pub async fn get_contest_by_id( logged_user.id, contest_id, )?; - let submissions = submission::get_submissions_user_by_contest( - &mut connection, - logged_user.id, - contest_id, - )?; + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; render( &hb, @@ -372,11 +245,8 @@ async fn get_contest_scoreboard_by_id( let contest = contest::get_contest_by_id(&mut connection, contest_id)?; assert_contest_not_started(&logged_user, &contest)?; let scores = problem::get_problems_by_contest_id_with_score(&mut connection, contest_id)?; - let submissions = submission::get_submissions_user_by_contest( - &mut connection, - logged_user.id, - contest_id, - )?; + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; let mut scores: Vec<_> = scores.into_iter().group_by(|e| e.user_name.as_ref().map(|s| s.clone())).into_iter().map(|(user_name, problems)| { let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); @@ -513,13 +383,6 @@ async fn get_contest_problem_by_id_label( ) } -#[derive(Serialize, Deserialize, Clone)] -struct LoggedUser { - id: i32, - name: String, - is_admin: bool, -} - #[post("/logout")] async fn post_logout(identity: Identity) -> PageResult { identity.logout(); @@ -557,9 +420,7 @@ async fn post_login( name: (&logged_user.name).into(), is_admin: logged_user.is_admin, }) - .map_err(|_| { - PageError::Custom("Usuário no banco de dados inconsistente".into()) - })?, + .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, ) .map_err(|_| PageError::Custom("Impossível fazer login".into()))?; Ok(redirect_to_root()) @@ -773,11 +634,8 @@ async fn get_submissions_me_by_contest_id( submissions: Vec<FormattedSubmission>, } - let submissions = submission::get_submissions_user_by_contest( - &mut connection, - logged_user.id, - contest_id, - )?; + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; render( &hb, @@ -1029,8 +887,7 @@ async fn create_submission( test_count: metadata.test_count, test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern).into(), checker_language: metadata.checker_language, - checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path) - .into(), + checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path).into(), })), }) .await?; @@ -1157,37 +1014,6 @@ fn get_formatted_contests( }) } -#[get("/")] -async fn get_main( - base: BaseContext, - identity: Option<Identity>, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = get_identity(&identity.as_ref()); - - #[derive(Serialize)] - struct Context { - base: BaseContext, - contests: Vec<FormattedContest>, - submissions: Vec<FormattedSubmission>, - } - - let mut connection = pool.get()?; - let submissions = submission::get_submissions(&mut connection)?; - - render( - &hb, - "main", - &Context { - base, - contests: get_formatted_contests(&mut connection, logged_user.map(|u| u.id), &tz)?, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - #[get("/contests/")] async fn get_contests( base: BaseContext, @@ -1282,8 +1108,7 @@ async fn create_contest( form.grade_ratio = parse_field("grade_ratio", &mut cursor)?.parse().ok() } Some("grade_after_ratio") => { - form.grade_after_ratio = - parse_field("grade_after_ratio", &mut cursor)?.parse().ok() + form.grade_after_ratio = parse_field("grade_after_ratio", &mut cursor)?.parse().ok() } Some("polygon_zip") => form.polygon_zip = Some(cursor), _ => {} @@ -1399,9 +1224,7 @@ async fn create_contest( fn map_codeforces_language(input: &String) -> Result<String, PageError> { Ok(CODEFORCES_LANGUAGE_TO_JUGHISTO .get(input) - .ok_or_else(|| { - PageError::Validation(format!("Linguagem {} não suportada", input)) - })? + .ok_or_else(|| PageError::Validation(format!("Linguagem {} não suportada", input)))? .into()) } diff --git a/src/pages/prelude.rs b/src/pages/prelude.rs new file mode 100644 index 0000000000000000000000000000000000000000..30b722cbad50f09e529e5e21fc41b3e03fc3c743 --- /dev/null +++ b/src/pages/prelude.rs @@ -0,0 +1,141 @@ +pub use actix_identity::Identity; +pub use actix_web::web::Data; +pub use actix_web::{get, post, FromRequest, HttpResponse}; +use diesel::r2d2::ConnectionManager; +use diesel::PgConnection; +pub use handlebars::{Handlebars, RenderError}; +pub use serde::Serialize; + +pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; + +use std::env; +use std::future::Future; +use std::pin::Pin; + +use actix_web::dev::Payload; +use actix_web::http::header::ContentType; +use actix_web::http::StatusCode; +use actix_web::HttpRequest; +use actix_web_flash_messages::IncomingFlashMessages; +use log::error; +use serde::Deserialize; +use thiserror::Error; + +use crate::Job; + +#[derive(Serialize, Deserialize, Clone)] +pub struct LoggedUser { + pub id: i32, + pub name: String, + pub is_admin: bool, +} + +impl actix_web::error::ResponseError for PageError { + fn error_response(&self) -> HttpResponse { + error!("{}", self); + HttpResponse::build(self.status_code()) + .insert_header(ContentType::plaintext()) + .body(self.to_string()) + } + + fn status_code(&self) -> StatusCode { + match *self { + PageError::Unauthorized() => StatusCode::UNAUTHORIZED, + PageError::Validation(_) => StatusCode::BAD_REQUEST, + PageError::Forbidden(_) => StatusCode::FORBIDDEN, + PageError::Custom(_) + | PageError::SessionInsert(_) + | PageError::ConnectionPool(_) + | PageError::Web(_) + | PageError::Render(_) + | PageError::Queue(_) + | PageError::SessionGet(_) + | PageError::Database(_) + | PageError::Io(_) + | PageError::UserHashing(_) + | PageError::Zip(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Error, Debug)] +pub enum PageError { + #[error("Unauthorized")] + Unauthorized(), + #[error("Couldn't render: {0}")] + Render(#[from] handlebars::RenderError), + #[error(transparent)] + SessionGet(#[from] actix_session::SessionGetError), + #[error("{0}")] + Custom(String), + #[error("{0}")] + Forbidden(String), + #[error("{0}")] + Validation(String), + #[error("Couldn't get connection from pool")] + ConnectionPool(#[from] r2d2::Error), + #[error("Couldn't hash")] + UserHashing(#[from] crate::user::UserHashingError), + #[error(transparent)] + Web(#[from] actix_web::Error), + #[error(transparent)] + Queue(#[from] async_channel::SendError<Job>), + #[error("Couldn't fetch result from database")] + Database(#[from] diesel::result::Error), + #[error("couldn't insert session")] + SessionInsert(#[from] actix_session::SessionInsertError), + #[error("couldn't work with the filesystem")] + Io(#[from] std::io::Error), + #[error("couldn't work with the zip")] + Zip(#[from] zip::result::ZipError), +} + +pub fn get_identity(identity: &Option<&Identity>) -> Option<LoggedUser> { + identity.as_ref().and_then(|identity| { + let identity = identity.id(); + identity + .ok() + .and_then(|identity| serde_json::from_str(&identity).ok()) + }) +} + +pub fn require_identity(identity: &Identity) -> Result<LoggedUser, PageError> { + get_identity(&Some(identity)).ok_or(PageError::Unauthorized()) +} + +pub type PageResult = Result<HttpResponse, PageError>; + +#[derive(Serialize)] +pub struct BaseContext { + logged_user: Option<LoggedUser>, + flash_messages: IncomingFlashMessages, + base_url: String, +} + +impl FromRequest for BaseContext { + type Error = actix_web::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let identity = Option::<Identity>::from_request(&req, &mut Payload::None).await; + let logged_user = identity?.and_then(|identity| get_identity(&Some(&identity))); + let flash_messages = + IncomingFlashMessages::from_request(&req, &mut Payload::None).await?; + Ok(BaseContext { + logged_user, + flash_messages, + base_url: env::var("BASE_URL").expect("BASE_URL environment variable is not set"), + }) + }) + } +} + +pub fn render<Err: From<RenderError>, Ctx: serde::Serialize>( + hb: &Handlebars, + name: &str, + context: &Ctx, +) -> Result<HttpResponse, Err> { + Ok(HttpResponse::Ok().body(hb.render(name, context)?)) +}