diff --git a/src/main.rs b/src/main.rs
index fdbfed230fe498b01976b5c20a4ee974659d40d6..f166174ed6e71ad5a4a924abbc5e45ae278d6e79 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,65 +1,35 @@
-use std::collections::HashMap;
-use std::convert::TryFrom;
+use std::env;
 use std::error::Error;
-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::sync::Arc;
 use std::time::Duration;
-use std::{env, fs, str};
 
-use actix_files::{Files, NamedFile};
-use actix_identity::{Identity, IdentityMiddleware};
-use actix_multipart::Multipart;
+use actix_files::Files;
+use actix_identity::IdentityMiddleware;
 use actix_session::storage::CookieSessionStore;
-use actix_session::{Session, SessionMiddleware};
+use actix_session::SessionMiddleware;
 use actix_web::cookie::Key;
-use actix_web::dev::Payload;
-use actix_web::http::header::{ContentType, HeaderValue};
-use actix_web::http::{header, StatusCode};
-use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
+use actix_web::middleware::ErrorHandlers;
 use actix_web::web::Data;
-use actix_web::{
-    dev, get, http, middleware, post, web, App, FromRequest, HttpMessage, HttpRequest,
-    HttpResponse, HttpServer,
-};
+use actix_web::{http, middleware, web, App, HttpServer};
 use actix_web_flash_messages::storage::CookieMessageStore;
-use actix_web_flash_messages::{
-    FlashMessage, FlashMessagesFramework, IncomingFlashMessages, Level,
-};
-use async_channel::Sender;
-use base64;
+use actix_web_flash_messages::{FlashMessagesFramework, Level};
 use broadcaster::Broadcaster;
-use chrono::prelude::*;
 use chrono_tz::Tz;
-use contest::{Contest, ContestWithAcs};
 use dashmap::DashMap;
 use diesel::pg::PgConnection;
 use diesel::r2d2::ConnectionManager;
 use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
-use futures::{StreamExt, TryFutureExt, TryStreamExt};
-use handlebars::{Handlebars, RenderError};
-use itertools::Itertools;
-use lazy_static::lazy_static;
+use futures::TryFutureExt;
+use handlebars::Handlebars;
 use listenfd::ListenFd;
-use log::{error, info};
-use models::problem::ProblemByContest;
 use models::{contest, problem, submission, user};
-use problem::{ProblemByContestMetadata, ProblemByContestWithScore};
+use problem::ProblemByContestMetadata;
 use queue::job_protocol::job_queue_server::JobQueueServer;
 use queue::job_protocol::{job, job_result, Job, JobResult, Language};
 use queue::JobQueuer;
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use submission::{ContestProblem, Submission, SubmissionCompletion};
-use thiserror::Error;
+use submission::{Submission, SubmissionCompletion};
 use tokio::sync::broadcast;
 use tonic::transport::Server;
-use user::{PasswordMatched, User, UserHashingError};
-use uuid::Uuid;
 
 mod broadcaster;
 mod import_contest;
@@ -68,13 +38,14 @@ mod models;
 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<(), PageError> {
+) -> Result<(), pages::PageError> {
     loop {
         let job_result = job_result_receiver.recv().await.unwrap();
         if let JobResult {
@@ -204,8 +175,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
             .app_data(Data::new(job_result_sender_data.clone()))
             .app_data(Data::new(languages_data.clone()))
             .app_data(Data::new(tz.clone()))
-            .wrap(ErrorHandlers::new().handler(http::StatusCode::UNAUTHORIZED, render_401))
-            .wrap(ErrorHandlers::new().handler(http::StatusCode::BAD_REQUEST, render_400))
+            .wrap(ErrorHandlers::new().handler(http::StatusCode::UNAUTHORIZED, pages::render_401))
+            .wrap(ErrorHandlers::new().handler(http::StatusCode::BAD_REQUEST, pages::render_400))
             .wrap(
                 FlashMessagesFramework::builder(
                     CookieMessageStore::builder(Key::from(&private_key)).build(),
@@ -225,29 +196,29 @@ async fn main() -> Result<(), Box<dyn Error>> {
             .app_data(handlebars_ref.clone())
             .service(
                 web::scope("/jughisto")
-                    .service(get_login)
-                    .service(get_me)
-                    .service(change_password)
-                    .service(post_login)
-                    .service(post_logout)
-                    .service(get_main)
-                    .service(get_contests)
-                    .service(get_contest_by_id)
-                    .service(get_contest_scoreboard_by_id)
-                    .service(get_contest_problem_by_id_label)
-                    .service(get_submissions_me)
-                    .service(get_submission)
-                    .service(get_submissions)
-                    .service(get_submissions_me_by_contest_id)
-                    .service(get_submissions_me_by_contest_id_problem_label)
-                    .service(rejudge_submission)
-                    .service(create_submission)
-                    .service(create_contest)
-                    .service(create_user)
-                    .service(impersonate_user)
-                    .service(submission_updates)
-                    .service(Files::new("/static/", "./static/"))
-                    .service(get_problem_by_id_assets),
+                    .service(pages::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_contests)
+                    .service(pages::get_contest_by_id)
+                    .service(pages::get_contest_scoreboard_by_id)
+                    .service(pages::get_contest_problem_by_id_label)
+                    .service(pages::get_submissions_me)
+                    .service(pages::get_submission)
+                    .service(pages::get_submissions)
+                    .service(pages::get_submissions_me_by_contest_id)
+                    .service(pages::get_submissions_me_by_contest_id_problem_label)
+                    .service(pages::rejudge_submission)
+                    .service(pages::create_submission)
+                    .service(pages::create_contest)
+                    .service(pages::create_user)
+                    .service(pages::impersonate_user)
+                    .service(pages::submission_updates)
+                    .service(pages::get_problem_by_id_assets)
+                    .service(Files::new("/static/", "./static/")),
             )
     });
 
@@ -283,1496 +254,3 @@ async fn main() -> Result<(), Box<dyn Error>> {
 
     Ok(())
 }
-
-#[derive(Error, Debug)]
-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)]
-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"),
-            })
-        })
-    }
-}
-
-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)?))
-}
-
-#[get("/login")]
-async fn get_login(base: BaseContext, hb: web::Data<Handlebars<'_>>) -> PageResult {
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-    }
-    render(&hb, "login", &Context { base })
-}
-
-#[get("/me")]
-async fn get_me(base: BaseContext, identity: Identity, hb: web::Data<Handlebars<'_>>) -> PageResult {
-    require_identity(&identity)?;
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-    }
-    render(&hb, "me", &Context { base })
-}
-
-#[get("/problems/{id}/assets/{filename}")]
-async fn get_problem_by_id_assets(
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    path: web::Path<(i32, String)>,
-) -> Result<NamedFile, PageError> {
-    let logged_user = require_identity(&identity)?;
-    let (problem_id, asset_filename) = path.into_inner();
-    let mut connection = pool.get()?;
-    let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?;
-    let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?;
-    assert_contest_not_started(&logged_user, &contest)?;
-    let file_path = PathBuf::from("/data/")
-        .join(problem.id)
-        .join("statements/.html/portuguese/")
-        .join(asset_filename);
-    Ok(NamedFile::open(file_path)?)
-}
-
-fn render_401<B>(mut res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {
-    FlashMessage::error("Você precisa estar logado para acessar esta página").send();
-    res.response_mut().headers_mut().insert(
-        header::LOCATION,
-        HeaderValue::from_str(&format!(
-            "{}login",
-            &env::var("BASE_URL").expect("BASE_URL environment variable is not set")
-        ))
-        .unwrap(),
-    );
-    *res.response_mut().status_mut() = StatusCode::SEE_OTHER;
-    Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
-}
-
-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()))
-}
-
-#[derive(Serialize, Deserialize)]
-struct LoginForm {
-    name: String,
-    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}",
-        if duration.num_milliseconds() >= 0 {
-            ""
-        } else {
-            "-"
-        },
-        if duration.num_days() != 0 {
-            format!("{}:", duration.num_days().abs())
-        } else {
-            "".into()
-        },
-        duration.num_hours().abs() % 24,
-        duration.num_minutes().abs() % 60
-    )
-}
-
-#[derive(Serialize)]
-struct FormattedProblemByContestWithScore {
-    pub first_ac_submission_time: String,
-    pub first_ac_submission_minutes: Option<i64>,
-    pub user_accepted_count: i32,
-    pub failed_submissions: i32,
-    pub id: i32,
-    pub name: String,
-    pub label: String,
-    pub memory_limit_mib: i32,
-    pub time_limit: String,
-}
-
-fn get_formatted_problem_by_contest_with_score(
-    p: &ProblemByContestWithScore,
-    contest: &Contest,
-) -> FormattedProblemByContestWithScore {
-    FormattedProblemByContestWithScore {
-        first_ac_submission_time: p
-            .first_ac_submission_instant
-            .map(|t| match contest.start_instant {
-                Some(cs) => format_duration(t - cs),
-                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,
-            }
-        }),
-        failed_submissions: p.failed_submissions,
-        id: p.id,
-        name: p.name.clone(),
-        label: p.label.clone(),
-        memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024,
-        time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen(".", ",", 1),
-        user_accepted_count: p.user_accepted_count,
-    }
-}
-
-#[get("/contests/{id}")]
-async fn get_contest_by_id(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    path: web::Path<(i32,)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let (contest_id,) = path.into_inner();
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        contest: FormattedContest,
-        problems: Vec<FormattedProblemByContestWithScore>,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let mut connection = pool.get()?;
-    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
-    assert_contest_not_started(&logged_user, &contest)?;
-
-    let problems = problem::get_problems_user_by_contest_id_with_score(
-        &mut connection,
-        logged_user.id,
-        contest_id,
-    )?;
-    let submissions =
-        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
-
-    render(
-        &hb,
-        "contest",
-        &Context {
-            base,
-            contest: get_formatted_contest(&tz, &contest),
-            problems: problems
-                .iter()
-                .map(|p| get_formatted_problem_by_contest_with_score(p, &contest))
-                .collect(),
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[get("/contests/{id}/scoreboard")]
-async fn get_contest_scoreboard_by_id(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    path: web::Path<(i32,)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let (contest_id,) = path.into_inner();
-
-    #[derive(Serialize)]
-    struct Score {
-        pub user_name: Option<String>,
-        pub problems: Vec<FormattedProblemByContestWithScore>,
-        pub solved_count: i64,
-        pub penalty: i64,
-    }
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        contest: FormattedContest,
-        scores: Vec<Score>,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let mut connection = pool.get()?;
-    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 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();
-
-        Score {
-            user_name,
-            solved_count: i64::try_from(problems.iter().filter(|p| p.first_ac_submission_time != "").count()).unwrap(),
-            penalty: problems.iter().filter(|p| p.first_ac_submission_time != "")
-                .map(|p| match p.first_ac_submission_minutes {
-                    Some(x) if x >= 0 => x,
-                    _ => 0,
-                } + i64::from(20*p.failed_submissions))
-                .sum(),
-                problems
-        }
-    }).collect();
-    scores.sort_by(|a, b| {
-        (
-            a.user_name != None,
-            -a.solved_count,
-            a.penalty,
-            &a.user_name,
-        )
-            .cmp(&(
-                b.user_name != None,
-                -b.solved_count,
-                b.penalty,
-                &b.user_name,
-            ))
-    });
-
-    render(
-        &hb,
-        "scoreboard",
-        &Context {
-            base,
-            contest: get_formatted_contest(&tz, &contest),
-            scores,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-fn assert_contest_not_started(logged_user: &LoggedUser, contest: &Contest) -> Result<(), PageError> {
-    if contest
-        .start_instant
-        .map(|s| s > Local::now().naive_utc())
-        .unwrap_or(false)
-        && !logged_user.is_admin
-    {
-        return Err(PageError::Forbidden(
-            "Essa competição ainda não começou".into(),
-        ));
-    }
-    Ok(())
-}
-
-#[get("/contests/{id}/{label}")]
-async fn get_contest_problem_by_id_label(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    languages: web::Data<Arc<DashMap<String, Language>>>,
-    session: Session,
-    path: web::Path<(i32, String)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let (contest_id, problem_label) = path.into_inner();
-
-    #[derive(Serialize, Debug)]
-    struct LanguageContext {
-        order: i32,
-        name: String,
-        value: String,
-    }
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        languages: Vec<LanguageContext>,
-        language: Option<String>,
-        contest: FormattedContest,
-        problems: Vec<ProblemByContest>,
-        problem: ProblemByContest,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let mut languages = languages
-        .iter()
-        .filter(|kv| {
-            if logged_user.is_admin {
-                true
-            } else {
-                kv.key() == "cpp.17.g++"
-            }
-        })
-        .map(|kv| LanguageContext {
-            order: kv.value().order,
-            value: kv.key().into(),
-            name: kv.value().name.clone(),
-        })
-        .collect::<Vec<_>>();
-    languages.sort_by(|a, b| a.order.cmp(&b.order));
-
-    let mut connection = pool.get()?;
-    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
-    assert_contest_not_started(&logged_user, &contest)?;
-    let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
-    let problem =
-        problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?;
-    let submissions = submission::get_submissions_user_by_contest_problem(
-        &mut connection,
-        logged_user.id,
-        contest_id,
-        &problem_label,
-    )?;
-
-    render(
-        &hb,
-        "contest_problem",
-        &Context {
-            base,
-            contest: get_formatted_contest(&tz, &contest),
-            languages,
-            problems,
-            problem,
-            language: session.get("language")?,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-struct LoggedUser {
-    id: i32,
-    name: String,
-    is_admin: bool,
-}
-
-#[post("/logout")]
-async fn post_logout(identity: Identity) -> PageResult {
-    identity.logout();
-    Ok(redirect_to_root())
-}
-
-#[post("/login")]
-async fn post_login(
-    pool: web::Data<DbPool>,
-    form: web::Form<LoginForm>,
-    request: HttpRequest,
-) -> PageResult {
-    let mut connection = pool.get()?;
-
-    match web::block(move || {
-        user::check_matching_password(&mut connection, &form.name, &form.password)
-    })
-    .await
-    .map_err(|e| PageError::Web(e.into()))?
-    .map_err(|e| match e {
-        UserHashingError::Database(e) => PageError::Database(e),
-        UserHashingError::Hash(_) => PageError::Validation("Senha inválida".into()),
-    })? {
-        PasswordMatched::UserDoesntExist => {
-            Err(PageError::Validation("Usuário inexistente".into()))
-        }
-        PasswordMatched::PasswordDoesntMatch => {
-            Err(PageError::Validation("Senha incorreta".into()))
-        }
-        PasswordMatched::PasswordMatches(logged_user) => {
-            Identity::login(
-                &request.extensions(),
-                serde_json::to_string(&LoggedUser {
-                    id: logged_user.id,
-                    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("Impossível fazer login".into()))?;
-            Ok(redirect_to_root())
-        }
-    }
-}
-
-#[get("/submission_updates/")]
-async fn submission_updates(broadcaster: web::Data<Mutex<Broadcaster>>) -> HttpResponse {
-    let rx = broadcaster
-        .lock()
-        .expect("Submission broadcaster is not active")
-        .new_client();
-
-    HttpResponse::Ok()
-        .append_header(("content-type", "text/event-stream"))
-        .streaming(rx)
-}
-
-#[derive(Serialize)]
-struct FormattedSubmission {
-    uuid: String,
-    verdict: String,
-    problem_label: String,
-    submission_instant: String,
-    error_output: Option<String>,
-    user_name: String,
-    time_ms: Option<i32>,
-    memory: String,
-    failed_test: Option<i32>,
-}
-
-fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String {
-    tz.from_utc_datetime(&input)
-        .format("%d/%m/%Y %H:%M:%S")
-        .to_string()
-}
-
-fn get_formatted_submissions(
-    tz: &Tz,
-    vec: &Vec<(Submission, ContestProblem, User)>,
-) -> Vec<FormattedSubmission> {
-    vec.iter()
-        .map(|(submission, contest_problem, user)| FormattedSubmission {
-            uuid: (&submission.uuid).into(),
-            verdict: submission
-                .verdict
-                .as_ref()
-                .map(|s| String::from(s))
-                .unwrap_or("WJ".into())
-                .into(),
-            problem_label: contest_problem.label.clone(),
-            submission_instant: format_utc_date_time(tz, submission.submission_instant),
-            error_output: submission.error_output.as_ref().map(|s| s.into()),
-            user_name: user.name.clone(),
-            time_ms: submission.time_ms,
-            memory: match submission.memory_kib {
-                None | Some(0) => "".into(),
-                Some(k) if k < 1_024 => format!("{}KiB", k),
-                Some(k) => format!("{}MiB", k / 1_024),
-            },
-            failed_test: submission.failed_test,
-        })
-        .collect()
-}
-
-#[get("/submissions/")]
-async fn get_submissions(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let mut connection = pool.get()?;
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let submissions = if logged_user.is_admin {
-        submission::get_submissions(&mut connection)?
-    } else {
-        submission::get_submissions_user(&mut connection, logged_user.id)?
-    };
-
-    render(
-        &hb,
-        "submissions",
-        &Context {
-            base,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[get("/submissions/me/")]
-async fn get_submissions_me(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let mut connection = pool.get()?;
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let submissions = submission::get_submissions_user(&mut connection, logged_user.id)?;
-
-    render(
-        &hb,
-        "submissions",
-        &Context {
-            base,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[get("/submissions/{uuid}")]
-async fn get_submission(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    path: web::Path<(String,)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let (submission_uuid,) = path.into_inner();
-    let mut connection = pool.get()?;
-
-    #[derive(Serialize)]
-    struct Submission {
-        pub uuid: String,
-        pub verdict: Option<String>,
-        pub source_text: String,
-        pub language: String,
-        pub memory_kib: Option<i32>,
-        pub time_ms: Option<i32>,
-        pub time_wall_ms: Option<i32>,
-        pub error_output: Option<String>,
-        pub user_name: String,
-        pub problem_label: String,
-        pub contest_name: String,
-        pub failed_test: Option<i32>,
-    }
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        submission: Submission,
-    }
-    let (submission, user, contest_problem, contest) =
-        submission::get_submission_by_uuid(&mut connection, submission_uuid)?;
-
-    if user.id != logged_user.id && !logged_user.is_admin {
-        return Err(PageError::Forbidden(
-            "Não é possível acessar uma submissão de outro usuário".into(),
-        ));
-    }
-
-    render(
-        &hb,
-        "submission",
-        &Context {
-            base,
-            submission: Submission {
-                uuid: submission.uuid,
-                verdict: submission.verdict,
-                source_text: submission.source_text,
-                language: submission.language,
-                memory_kib: submission.memory_kib,
-                time_ms: submission.time_ms,
-                time_wall_ms: submission.time_wall_ms,
-                error_output: submission.error_output,
-                user_name: user.name,
-                problem_label: contest_problem.label,
-                contest_name: contest.name,
-                failed_test: submission.failed_test,
-            },
-        },
-    )
-}
-
-#[get("/submissions/me/contests/{id}")]
-async fn get_submissions_me_by_contest_id(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    path: web::Path<(i32,)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let (contest_id,) = path.into_inner();
-    let mut connection = pool.get()?;
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let submissions =
-        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
-
-    render(
-        &hb,
-        "submissions",
-        &Context {
-            base,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[get("/submissions/me/contests/{id}/{label}")]
-async fn get_submissions_me_by_contest_id_problem_label(
-    base: BaseContext,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    path: web::Path<(i32, String)>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let mut connection = pool.get()?;
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        submissions: Vec<FormattedSubmission>,
-    }
-
-    let (contest_id, problem_label) = path.into_inner();
-    let submissions = submission::get_submissions_user_by_contest_problem(
-        &mut connection,
-        logged_user.id,
-        contest_id,
-        &problem_label,
-    )?;
-
-    render(
-        &hb,
-        "submissions",
-        &Context {
-            base,
-            submissions: get_formatted_submissions(&tz, &submissions),
-        },
-    )
-}
-
-#[derive(Serialize, Deserialize)]
-struct SubmissionForm {
-    contest_problem_id: i32,
-    language: String,
-    source_text: String,
-}
-
-fn redirect_to_root() -> HttpResponse {
-    HttpResponse::SeeOther()
-        .append_header((
-            header::LOCATION,
-            HeaderValue::from_str(
-                &env::var("BASE_URL").expect("BASE_URL environment variable is not set"),
-            )
-            .unwrap(),
-        ))
-        .finish()
-}
-
-fn redirect_to_referer(message: String, request: &HttpRequest) -> HttpResponse {
-    let referer = request
-        .headers()
-        .get("Referer")
-        .and_then(|h| h.to_str().ok())
-        .map(|s| s.into())
-        .unwrap_or(env::var("BASE_URL").expect("BASE_URL environment variable is not set"));
-    FlashMessage::info(message).send();
-    HttpResponse::SeeOther()
-        .append_header((header::LOCATION, HeaderValue::from_str(&referer).unwrap()))
-        .finish()
-}
-
-#[derive(Serialize, Deserialize)]
-struct ChangePasswordForm {
-    old_password: String,
-    new_password: String,
-    new_password_repeat: String,
-}
-
-#[post("/me/password")]
-async fn change_password(
-    identity: Identity,
-    form: web::Form<ChangePasswordForm>,
-    pool: web::Data<DbPool>,
-    request: HttpRequest,
-) -> PageResult {
-    let identity = require_identity(&identity)?;
-    if form.new_password != form.new_password_repeat {
-        return Err(PageError::Validation("Senhas são diferentes".into()));
-    }
-
-    let mut connection = pool.get()?;
-
-    match user::change_password(
-        &mut connection,
-        identity.id,
-        &form.old_password,
-        &form.new_password,
-    )? {
-        PasswordMatched::PasswordMatches(_) => Ok(redirect_to_referer(
-            "Senha alterada com sucesso".into(),
-            &request,
-        )),
-        _ => Ok(redirect_to_referer(
-            "Senha antiga incorreta".into(),
-            &request,
-        )),
-    }
-}
-
-#[derive(Serialize, Deserialize)]
-struct CreateUserForm {
-    name: String,
-    password: String,
-    is_admin: Option<bool>,
-}
-
-#[post("/users/")]
-async fn create_user(
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    form: web::Form<CreateUserForm>,
-    request: HttpRequest,
-) -> PageResult {
-    let identity = require_identity(&identity)?;
-    if !identity.is_admin {
-        return Err(PageError::Forbidden(
-            "Apenas administradores podem fazer isso".into(),
-        ));
-    }
-
-    let mut connection = pool.get()?;
-
-    user::insert_new_user(
-        &mut connection,
-        user::NewUser {
-            name: &form.name,
-            password: &form.password,
-            is_admin: form.is_admin.unwrap_or(false),
-            creation_instant: Local::now().naive_utc(),
-            creation_user_id: Some(identity.id),
-        },
-    )?;
-
-    Ok(redirect_to_referer(
-        "Usuário criado com sucesso".into(),
-        &request,
-    ))
-}
-
-#[derive(Serialize, Deserialize)]
-struct ImpersonateUserForm {
-    name: String,
-}
-
-#[post("/impersonate/")]
-async fn impersonate_user(
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    form: web::Form<ImpersonateUserForm>,
-    request: HttpRequest,
-) -> PageResult {
-    let my_identity = require_identity(&identity)?;
-    if !my_identity.is_admin {
-        return Err(PageError::Forbidden(
-            "Apenas administradores podem fazer isso".into(),
-        ));
-    }
-
-    let mut connection = pool.get()?;
-
-    let user = user::get_user_by_name(&mut connection, &form.name)?;
-    Identity::login(
-        &request.extensions(),
-        serde_json::to_string(&LoggedUser {
-            id: user.id,
-            name: (&user.name).into(),
-            is_admin: user.is_admin,
-        })
-        .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?,
-    )
-    .map_err(|_| PageError::Custom("Impossível fazer login".into()))?;
-
-    Ok(redirect_to_referer(
-        "Personificado com sucesso".into(),
-        &request,
-    ))
-}
-
-#[post("/submissions/")]
-async fn create_submission(
-    identity: Identity,
-    form: web::Form<SubmissionForm>,
-    pool: web::Data<DbPool>,
-    job_sender: web::Data<Sender<Job>>,
-    languages: web::Data<Arc<DashMap<String, Language>>>,
-    session: Session,
-    request: HttpRequest,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let mut connection = pool.get()?;
-
-    languages
-        .get(&form.language)
-        .ok_or(PageError::Validation("Linguagem inexistente".into()))?;
-
-    if !logged_user.is_admin && form.language != "cpp.17.g++" {
-        return Err(PageError::Validation(
-            "Somente é possível submeter em C++".into(),
-        ));
-    }
-
-    let uuid = Uuid::new_v4();
-    submission::insert_submission(
-        &mut connection,
-        submission::NewSubmission {
-            uuid: uuid.to_string(),
-            source_text: (&form.source_text).into(),
-            language: (&form.language).into(),
-            submission_instant: Local::now().naive_utc(),
-            contest_problem_id: form.contest_problem_id,
-            user_id: logged_user.id,
-        },
-    )?;
-
-    let contest =
-        contest::get_contest_by_contest_problem_id(&mut connection, form.contest_problem_id)?;
-    assert_contest_not_started(&logged_user, &contest)?;
-
-    let metadata =
-        problem::get_problem_by_contest_id_metadata(&mut connection, form.contest_problem_id)?;
-
-    job_sender
-        .send(Job {
-            uuid: uuid.to_string(),
-            language: (&form.language).into(),
-            time_limit_ms: metadata.time_limit_ms,
-            memory_limit_kib: metadata.memory_limit_bytes / 1_024,
-
-            which: Some(job::Which::Judgement(job::Judgement {
-                source_text: (&form.source_text).into(),
-                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(),
-            })),
-        })
-        .await?;
-
-    session.insert("language", &form.language)?;
-    Ok(redirect_to_referer(
-        format!("Submetido {} com sucesso!", uuid),
-        &request,
-    ))
-}
-
-#[post("/submissions/{uuid}/rejudge")]
-async fn rejudge_submission(
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    job_sender: web::Data<Sender<Job>>,
-    request: HttpRequest,
-    path: web::Path<(String,)>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    let mut connection = pool.get()?;
-
-    if !logged_user.is_admin {
-        return Err(PageError::Forbidden(
-            "Apenas administradores podem fazer isso".into(),
-        ));
-    }
-
-    let (submission_uuid,) = path.into_inner();
-    let (submission, _, _, _) =
-        submission::get_submission_by_uuid(&mut connection, submission_uuid.clone())?;
-    let metadata = problem::get_problem_by_contest_id_metadata(
-        &mut connection,
-        submission.contest_problem_id,
-    )?;
-
-    job_sender
-        .send(create_job_from_submission(submission, metadata))
-        .await?;
-
-    Ok(redirect_to_referer(
-        format!("Rejulgando {}", submission_uuid),
-        &request,
-    ))
-}
-
-#[derive(Serialize)]
-struct FormattedContest {
-    pub id: i32,
-    pub name: String,
-    pub start_instant: Option<String>,
-    pub end_instant: Option<String>,
-    pub creation_instant: String,
-    pub grade_ratio: Option<i32>,
-    pub grade_after_ratio: Option<i32>,
-    pub accepted_count: i32,
-    pub accepted_after_count: i32,
-    pub accepted_total_count: i32,
-    pub problem_count: i32,
-    pub grade: String,
-}
-
-fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest {
-    FormattedContest {
-        id: contest.id,
-        name: contest.name.clone(),
-        start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)),
-        end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)),
-        creation_instant: format_utc_date_time(&tz, contest.creation_instant),
-        grade_ratio: contest.grade_ratio,
-        grade_after_ratio: contest.grade_after_ratio,
-        accepted_count: 0,
-        accepted_after_count: 0,
-        accepted_total_count: 0,
-        problem_count: 0,
-        grade: "".into(),
-    }
-}
-
-fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedContest {
-    FormattedContest {
-        id: contest.id,
-        name: contest.name.clone(),
-        start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)),
-        end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)),
-        creation_instant: format_utc_date_time(&tz, contest.creation_instant),
-        grade_ratio: contest.grade_ratio,
-        grade_after_ratio: contest.grade_after_ratio,
-        accepted_count: contest.accepted_count,
-        accepted_after_count: contest.accepted_after_count,
-        accepted_total_count: contest.accepted_count + contest.accepted_after_count,
-        problem_count: contest.problem_count,
-        grade: match contest.grade_ratio {
-            Some(grade_ratio) => format!(
-                "{:.2}",
-                10.0 * (f64::from(contest.accepted_count) * 1.0 / f64::from(grade_ratio)
-                    + match contest.grade_after_ratio {
-                        Some(grade_after_ratio) =>
-                            f64::from(contest.accepted_after_count) * 1.0
-                                / f64::from(grade_after_ratio),
-                        None => 0.0,
-                    })
-            )
-            .replacen(".", ",", 1),
-            None => "".into(),
-        },
-    }
-}
-
-fn get_formatted_contests(
-    connection: &mut PgConnection,
-    user_id: Option<i32>,
-    tz: &Tz,
-) -> Result<Vec<FormattedContest>, PageError> {
-    Ok(match user_id {
-        Some(user_id) => contest::get_contests_with_acs(connection, user_id)?
-            .iter()
-            .map(|c| get_formatted_contest_acs(tz, c))
-            .collect(),
-        None => contest::get_contests(connection)?
-            .iter()
-            .map(|c| get_formatted_contest(tz, c))
-            .collect(),
-    })
-}
-
-#[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,
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    hb: web::Data<Handlebars<'_>>,
-    tz: web::Data<Tz>,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-
-    #[derive(Serialize)]
-    struct Context {
-        base: BaseContext,
-        contests: Vec<FormattedContest>,
-    }
-
-    let mut connection = pool.get()?;
-    render(
-        &hb,
-        "contests",
-        &Context {
-            base,
-            contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?,
-        },
-    )
-}
-
-#[post("/contests/")]
-async fn create_contest(
-    identity: Identity,
-    pool: web::Data<DbPool>,
-    mut payload: Multipart,
-    job_sender: web::Data<Sender<Job>>,
-    job_result_sender: web::Data<broadcast::Sender<JobResult>>,
-    tz: web::Data<Tz>,
-    request: HttpRequest,
-) -> PageResult {
-    let logged_user = require_identity(&identity)?;
-    if !logged_user.is_admin {
-        return Err(PageError::Forbidden(
-            "Apenas administradores podem fazer isso".into(),
-        ));
-    }
-
-    #[derive(Debug)]
-    struct Form {
-        name: Option<String>,
-        start_instant: Option<String>,
-        end_instant: Option<String>,
-        polygon_zip: Option<Cursor<Vec<u8>>>,
-        grade_ratio: Option<i32>,
-        grade_after_ratio: Option<i32>,
-    }
-
-    let mut form = Form {
-        name: None,
-        start_instant: None,
-        end_instant: None,
-        polygon_zip: None,
-        grade_ratio: None,
-        grade_after_ratio: None,
-    };
-
-    while let Ok(Some(mut field)) = payload.try_next().await {
-        let mut cursor = Cursor::new(vec![]);
-        while let Some(chunk) = field.next().await {
-            let data = chunk.unwrap();
-            cursor
-                .write(&data)
-                .map_err(|_| PageError::Validation("Corpo inválido".into()))?;
-        }
-
-        cursor.set_position(0);
-
-        fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> {
-            let mut value = String::new();
-            cursor
-                .read_to_string(&mut value)
-                .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?;
-            Ok(value)
-        }
-
-        match field.content_disposition().get_name() {
-            Some("name") => form.name = Some(parse_field("name", &mut cursor)?),
-            Some("start_instant") => {
-                form.start_instant = Some(parse_field("start_instant", &mut cursor)?)
-            }
-            Some("end_instant") => {
-                form.end_instant = Some(parse_field("end_instant", &mut cursor)?)
-            }
-            Some("grade_ratio") => {
-                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()
-            }
-            Some("polygon_zip") => form.polygon_zip = Some(cursor),
-            _ => {}
-        }
-    }
-
-    let polygon_zip = form
-        .polygon_zip
-        .ok_or(PageError::Validation("Arquivo não informado".into()))?;
-    let imported = import_contest::import_file(polygon_zip)
-        .map_err(|e| PageError::Validation(format!("Não foi possível importar: {}", e)))?;
-    let mut connection = pool.get()?;
-
-    let contest = if form.name.as_ref().unwrap() != "" {
-        Some(contest::insert_contest(
-            &mut connection,
-            contest::NewContest {
-                name: form.name.clone().unwrap(),
-                start_instant: form
-                    .start_instant
-                    .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
-                    .map(|d| d.naive_utc()),
-                end_instant: form
-                    .end_instant
-                    .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
-                    .map(|d| d.naive_utc()),
-                creation_instant: Local::now().naive_utc(),
-                creation_user_id: logged_user.id,
-                grade_ratio: form.grade_ratio,
-                grade_after_ratio: form.grade_after_ratio,
-            },
-        )?)
-    } else {
-        None
-    };
-
-    fn polygon_url_to_id_without_revision(url: String) -> String {
-        url.replace("https://polygon.codeforces.com/", "polygon.")
-            .replace("/", ".")
-    }
-
-    let problem_label: HashMap<String, String> =
-        HashMap::from_iter(imported.0.problems.problem.iter().map(|problem| {
-            (
-                polygon_url_to_id_without_revision(problem.url.clone()),
-                problem.index.clone(),
-            )
-        }));
-
-    let mut zip = imported.2;
-
-    lazy_static! {
-        static ref CODEFORCES_LANGUAGE_TO_JUGHISTO: HashMap<String, String> = {
-            let mut m = HashMap::new();
-            m.insert("cpp.g++17".into(), "cpp.17.g++".into());
-            m.insert("cpp.msys2-mingw64-9-g++17".into(), "cpp.17.g++".into());
-            m.insert("java.8".into(), "java.8".into());
-            m.insert("testlib".into(), "cpp.17.g++".into());
-            m
-        };
-    }
-
-    for (name, metadata) in imported.1 {
-        let problem_id_without_revision = polygon_url_to_id_without_revision(metadata.url);
-        let problem_id = format!("{}.r{}", problem_id_without_revision, &metadata.revision);
-
-        let files_regex: Regex = Regex::new(&format!(
-            concat!(
-                "^{}/(",
-                r"files/$|",
-                r"files/.*\.cpp$|",
-                r"files/.*\.h$|",
-                r"files/tests/$|",
-                r"files/tests/validator-tests/$|",
-                r"files/tests/validator-tests/.*$|",
-                r"files/tests/validator-tests/.*$|",
-                r"solutions/$|",
-                r"solutions/.*.cc$|",
-                r"solutions/.*.cpp$|",
-                r"statements/$|",
-                r"statements/.html/.*$|",
-                r"tests/$",
-                ")"
-            ),
-            name
-        ))
-        .unwrap();
-        let mut filenames = zip
-            .file_names()
-            .filter(|name| files_regex.is_match(name))
-            .map(|s| s.to_string())
-            .collect::<Vec<_>>();
-        filenames.sort();
-        for name in filenames {
-            let relative_path = files_regex
-                .captures(&name)
-                .unwrap()
-                .get(1)
-                .unwrap()
-                .as_str();
-            let data_path = format!("/data/{}/{}", problem_id, relative_path);
-
-            if name.ends_with("/") {
-                info!("Creating directory {} into {}", name, data_path);
-                create_dir_all(data_path)?;
-                continue;
-            }
-
-            info!("Putting file {} into {}", name, data_path);
-            std::io::copy(&mut zip.by_name(&name)?, &mut File::create(data_path)?)?;
-        }
-
-        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)))?
-                .into())
-        }
-
-        let main_solution = &metadata
-            .assets
-            .solutions
-            .solution
-            .iter()
-            .find(|s| s.tag == "main")
-            .ok_or(PageError::Validation("No main solution".into()))?
-            .source;
-
-        let problem = problem::upsert_problem(
-            &mut connection,
-            problem::NewProblem {
-                id: problem_id.clone(),
-                name: metadata.names.name[0].value.clone(),
-                memory_limit_bytes: metadata.judging.testset[0]
-                    .memory_limit
-                    .value
-                    .parse()
-                    .unwrap(),
-                time_limit_ms: metadata.judging.testset[0]
-                    .time_limit
-                    .value
-                    .parse()
-                    .unwrap(),
-                checker_path: metadata.assets.checker.source.path.clone(),
-                checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?,
-                validator_path: metadata.assets.validators.validator[0].source.path.clone(),
-                validator_language: map_codeforces_language(
-                    &metadata.assets.validators.validator[0].source.r#type,
-                )?,
-                main_solution_path: main_solution.path.clone(),
-                main_solution_language: map_codeforces_language(&main_solution.r#type)?,
-                test_pattern: metadata.judging.testset[0].input_path_pattern.value.clone(),
-                test_count: metadata.judging.testset[0]
-                    .test_count
-                    .value
-                    .parse()
-                    .unwrap(),
-                status: "compiled".into(),
-                creation_instant: Local::now().naive_utc(),
-                creation_user_id: logged_user.id,
-            },
-        )?;
-
-        for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() {
-            let i = i + 1;
-            let test_path = format!(
-                "./{}/{}",
-                problem_id,
-                import_contest::format_width(&problem.test_pattern, i)
-            );
-
-            info!(
-                "Iterating through test {} to {:#?}, which is {}",
-                i,
-                test_path,
-                test.method.as_ref().unwrap()
-            );
-            if test.method.as_ref().unwrap() == "manual" {
-                let test_name = PathBuf::from(&name)
-                    .join(import_contest::format_width(&problem.test_pattern, i));
-                info!("Extracting {:#?} from zip", test_name);
-                std::io::copy(
-                    &mut zip.by_name(&test_name.to_str().unwrap())?,
-                    &mut File::create(PathBuf::from("/data/").join(&test_path))?,
-                )?;
-            } else {
-                let cmd: Vec<_> = test.cmd.as_ref().unwrap().split(" ").collect();
-                let run_stats = language::run_cached(
-                    &job_sender,
-                    &job_result_sender,
-                    &"cpp.17.g++".into(),
-                    format!("./{}/files/{}.cpp", problem.id, cmd.get(0).unwrap()),
-                    cmd[1..].iter().map(|s| s.clone().into()).collect(),
-                    None,
-                    Some(test_path.clone()),
-                    problem.memory_limit_bytes / 1_024,
-                    10_000,
-                )
-                .await
-                .map_err(|_| {
-                    PageError::Validation("Couldn't use an intermediate program".into())
-                })?;
-
-                if run_stats.result != i32::from(job_result::run_cached::Result::Ok) {
-                    return Err(PageError::Validation(
-                        "Couldn't run an intermediate program".into(),
-                    ));
-                }
-            }
-
-            let run_stats = language::run_cached(
-                &job_sender,
-                &job_result_sender,
-                &problem.main_solution_language,
-                format!("./{}/{}", problem.id, problem.main_solution_path),
-                vec![],
-                Some(test_path.clone()),
-                Some(format!("{}.a", test_path)),
-                problem.memory_limit_bytes / 1_024,
-                problem.time_limit_ms,
-            )
-            .await
-            .map_err(|_| PageError::Validation("Couldn't run solution on test".into()))?;
-            if run_stats.exit_code != 0 {
-                return Err(PageError::Validation(
-                    "Couldn't run solution on test".into(),
-                ));
-            }
-        }
-
-        language::judge(
-            &job_sender,
-            &job_result_sender,
-            &problem.main_solution_language,
-            fs::read_to_string(PathBuf::from(format!(
-                "/data/{}/{}",
-                problem.id, problem.main_solution_path
-            )))?,
-            problem.test_count,
-            format!("./{}/{}", problem.id, problem.test_pattern).into(),
-            problem.checker_language,
-            format!("./{}/{}", problem.id, problem.checker_path).into(),
-            problem.memory_limit_bytes / 1_024,
-            problem.time_limit_ms,
-        )
-        .await
-        .map_err(|_| PageError::Validation("Couldn't judge main solution".into()))?;
-
-        if form.name.as_ref().unwrap() != "" {
-            contest::relate_problem(
-                &mut connection,
-                contest::NewContestProblems {
-                    label: problem_label
-                        .get(&problem_id_without_revision)
-                        .ok_or(PageError::Validation(
-                            "Arquivo não contém problemas listados".into(),
-                        ))?
-                        .to_string()
-                        .to_uppercase(),
-                    contest_id: contest.as_ref().unwrap().id,
-                    problem_id,
-                },
-            )?;
-        }
-    }
-
-    if form.name.as_ref().unwrap() != "" {
-        Ok(redirect_to_referer(
-            "Competição criada com sucesso".into(),
-            &request,
-        ))
-    } else {
-        Ok(redirect_to_referer(
-            "Problemas adicionados com sucesso".into(),
-            &request,
-        ))
-    }
-}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cff4464660a6e6856d3f5cc665fd4e467dde1aa6
--- /dev/null
+++ b/src/pages/mod.rs
@@ -0,0 +1,1566 @@
+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};
+
+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, 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 async_channel::Sender;
+use chrono::prelude::*;
+use chrono_tz::Tz;
+use contest::{Contest, ContestWithAcs};
+use dashmap::DashMap;
+use diesel::pg::PgConnection;
+use futures::{StreamExt, TryStreamExt};
+use handlebars::{Handlebars, RenderError};
+use itertools::Itertools;
+use lazy_static::lazy_static;
+use log::{error, 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;
+
+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"),
+            })
+        })
+    }
+}
+
+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)?))
+}
+
+#[get("/login")]
+async fn get_login(base: BaseContext, hb: web::Data<Handlebars<'_>>) -> PageResult {
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+    }
+    render(&hb, "login", &Context { base })
+}
+
+#[get("/me")]
+async fn get_me(
+    base: BaseContext,
+    identity: Identity,
+    hb: web::Data<Handlebars<'_>>,
+) -> PageResult {
+    require_identity(&identity)?;
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+    }
+    render(&hb, "me", &Context { base })
+}
+
+#[get("/problems/{id}/assets/{filename}")]
+async fn get_problem_by_id_assets(
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    path: web::Path<(i32, String)>,
+) -> Result<NamedFile, PageError> {
+    let logged_user = require_identity(&identity)?;
+    let (problem_id, asset_filename) = path.into_inner();
+    let mut connection = pool.get()?;
+    let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?;
+    let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+    let file_path = PathBuf::from("/data/")
+        .join(problem.id)
+        .join("statements/.html/portuguese/")
+        .join(asset_filename);
+    Ok(NamedFile::open(file_path)?)
+}
+
+pub fn render_401<B>(
+    mut res: dev::ServiceResponse<B>,
+) -> actix_web::Result<ErrorHandlerResponse<B>> {
+    FlashMessage::error("Você precisa estar logado para acessar esta página").send();
+    res.response_mut().headers_mut().insert(
+        header::LOCATION,
+        HeaderValue::from_str(&format!(
+            "{}login",
+            &env::var("BASE_URL").expect("BASE_URL environment variable is not set")
+        ))
+        .unwrap(),
+    );
+    *res.response_mut().status_mut() = StatusCode::SEE_OTHER;
+    Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
+}
+
+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()))
+}
+
+#[derive(Serialize, Deserialize)]
+struct LoginForm {
+    name: String,
+    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}",
+        if duration.num_milliseconds() >= 0 {
+            ""
+        } else {
+            "-"
+        },
+        if duration.num_days() != 0 {
+            format!("{}:", duration.num_days().abs())
+        } else {
+            "".into()
+        },
+        duration.num_hours().abs() % 24,
+        duration.num_minutes().abs() % 60
+    )
+}
+
+#[derive(Serialize)]
+struct FormattedProblemByContestWithScore {
+    pub first_ac_submission_time: String,
+    pub first_ac_submission_minutes: Option<i64>,
+    pub user_accepted_count: i32,
+    pub failed_submissions: i32,
+    pub id: i32,
+    pub name: String,
+    pub label: String,
+    pub memory_limit_mib: i32,
+    pub time_limit: String,
+}
+
+fn get_formatted_problem_by_contest_with_score(
+    p: &ProblemByContestWithScore,
+    contest: &Contest,
+) -> FormattedProblemByContestWithScore {
+    FormattedProblemByContestWithScore {
+        first_ac_submission_time: p
+            .first_ac_submission_instant
+            .map(|t| match contest.start_instant {
+                Some(cs) => format_duration(t - cs),
+                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,
+        }),
+        failed_submissions: p.failed_submissions,
+        id: p.id,
+        name: p.name.clone(),
+        label: p.label.clone(),
+        memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024,
+        time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen(".", ",", 1),
+        user_accepted_count: p.user_accepted_count,
+    }
+}
+
+#[get("/contests/{id}")]
+pub async fn get_contest_by_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    path: web::Path<(i32,)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        problems: Vec<FormattedProblemByContestWithScore>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+
+    let problems = problem::get_problems_user_by_contest_id_with_score(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+    let submissions = submission::get_submissions_user_by_contest(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+
+    render(
+        &hb,
+        "contest",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            problems: problems
+                .iter()
+                .map(|p| get_formatted_problem_by_contest_with_score(p, &contest))
+                .collect(),
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[get("/contests/{id}/scoreboard")]
+async fn get_contest_scoreboard_by_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    path: web::Path<(i32,)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct Score {
+        pub user_name: Option<String>,
+        pub problems: Vec<FormattedProblemByContestWithScore>,
+        pub solved_count: i64,
+        pub penalty: i64,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        scores: Vec<Score>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    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 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();
+
+    Score {
+        user_name,
+        solved_count: i64::try_from(problems.iter().filter(|p| p.first_ac_submission_time != "").count()).unwrap(),
+        penalty: problems.iter().filter(|p| p.first_ac_submission_time != "")
+            .map(|p| match p.first_ac_submission_minutes {
+                Some(x) if x >= 0 => x,
+                _ => 0,
+            } + i64::from(20*p.failed_submissions))
+            .sum(),
+            problems
+    }
+}).collect();
+    scores.sort_by(|a, b| {
+        (
+            a.user_name != None,
+            -a.solved_count,
+            a.penalty,
+            &a.user_name,
+        )
+            .cmp(&(
+                b.user_name != None,
+                -b.solved_count,
+                b.penalty,
+                &b.user_name,
+            ))
+    });
+
+    render(
+        &hb,
+        "scoreboard",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            scores,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+fn assert_contest_not_started(
+    logged_user: &LoggedUser,
+    contest: &Contest,
+) -> Result<(), PageError> {
+    if contest
+        .start_instant
+        .map(|s| s > Local::now().naive_utc())
+        .unwrap_or(false)
+        && !logged_user.is_admin
+    {
+        return Err(PageError::Forbidden(
+            "Essa competição ainda não começou".into(),
+        ));
+    }
+    Ok(())
+}
+
+#[get("/contests/{id}/{label}")]
+async fn get_contest_problem_by_id_label(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    languages: web::Data<Arc<DashMap<String, Language>>>,
+    session: Session,
+    path: web::Path<(i32, String)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id, problem_label) = path.into_inner();
+
+    #[derive(Serialize, Debug)]
+    struct LanguageContext {
+        order: i32,
+        name: String,
+        value: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        languages: Vec<LanguageContext>,
+        language: Option<String>,
+        contest: FormattedContest,
+        problems: Vec<ProblemByContest>,
+        problem: ProblemByContest,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut languages = languages
+        .iter()
+        .filter(|kv| {
+            if logged_user.is_admin {
+                true
+            } else {
+                kv.key() == "cpp.17.g++"
+            }
+        })
+        .map(|kv| LanguageContext {
+            order: kv.value().order,
+            value: kv.key().into(),
+            name: kv.value().name.clone(),
+        })
+        .collect::<Vec<_>>();
+    languages.sort_by(|a, b| a.order.cmp(&b.order));
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+    let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
+    let problem =
+        problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?;
+    let submissions = submission::get_submissions_user_by_contest_problem(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+        &problem_label,
+    )?;
+
+    render(
+        &hb,
+        "contest_problem",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            languages,
+            problems,
+            problem,
+            language: session.get("language")?,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct LoggedUser {
+    id: i32,
+    name: String,
+    is_admin: bool,
+}
+
+#[post("/logout")]
+async fn post_logout(identity: Identity) -> PageResult {
+    identity.logout();
+    Ok(redirect_to_root())
+}
+
+#[post("/login")]
+async fn post_login(
+    pool: web::Data<DbPool>,
+    form: web::Form<LoginForm>,
+    request: HttpRequest,
+) -> PageResult {
+    let mut connection = pool.get()?;
+
+    match web::block(move || {
+        user::check_matching_password(&mut connection, &form.name, &form.password)
+    })
+    .await
+    .map_err(|e| PageError::Web(e.into()))?
+    .map_err(|e| match e {
+        UserHashingError::Database(e) => PageError::Database(e),
+        UserHashingError::Hash(_) => PageError::Validation("Senha inválida".into()),
+    })? {
+        PasswordMatched::UserDoesntExist => {
+            Err(PageError::Validation("Usuário inexistente".into()))
+        }
+        PasswordMatched::PasswordDoesntMatch => {
+            Err(PageError::Validation("Senha incorreta".into()))
+        }
+        PasswordMatched::PasswordMatches(logged_user) => {
+            Identity::login(
+                &request.extensions(),
+                serde_json::to_string(&LoggedUser {
+                    id: logged_user.id,
+                    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("Impossível fazer login".into()))?;
+            Ok(redirect_to_root())
+        }
+    }
+}
+
+#[get("/submission_updates/")]
+async fn submission_updates(broadcaster: web::Data<Mutex<Broadcaster>>) -> HttpResponse {
+    let rx = broadcaster
+        .lock()
+        .expect("Submission broadcaster is not active")
+        .new_client();
+
+    HttpResponse::Ok()
+        .append_header(("content-type", "text/event-stream"))
+        .streaming(rx)
+}
+
+#[derive(Serialize)]
+struct FormattedSubmission {
+    uuid: String,
+    verdict: String,
+    problem_label: String,
+    submission_instant: String,
+    error_output: Option<String>,
+    user_name: String,
+    time_ms: Option<i32>,
+    memory: String,
+    failed_test: Option<i32>,
+}
+
+fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String {
+    tz.from_utc_datetime(&input)
+        .format("%d/%m/%Y %H:%M:%S")
+        .to_string()
+}
+
+fn get_formatted_submissions(
+    tz: &Tz,
+    vec: &Vec<(Submission, ContestProblem, User)>,
+) -> Vec<FormattedSubmission> {
+    vec.iter()
+        .map(|(submission, contest_problem, user)| FormattedSubmission {
+            uuid: (&submission.uuid).into(),
+            verdict: submission
+                .verdict
+                .as_ref()
+                .map(|s| String::from(s))
+                .unwrap_or("WJ".into())
+                .into(),
+            problem_label: contest_problem.label.clone(),
+            submission_instant: format_utc_date_time(tz, submission.submission_instant),
+            error_output: submission.error_output.as_ref().map(|s| s.into()),
+            user_name: user.name.clone(),
+            time_ms: submission.time_ms,
+            memory: match submission.memory_kib {
+                None | Some(0) => "".into(),
+                Some(k) if k < 1_024 => format!("{}KiB", k),
+                Some(k) => format!("{}MiB", k / 1_024),
+            },
+            failed_test: submission.failed_test,
+        })
+        .collect()
+}
+
+#[get("/submissions/")]
+async fn get_submissions(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let submissions = if logged_user.is_admin {
+        submission::get_submissions(&mut connection)?
+    } else {
+        submission::get_submissions_user(&mut connection, logged_user.id)?
+    };
+
+    render(
+        &hb,
+        "submissions",
+        &Context {
+            base,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[get("/submissions/me/")]
+async fn get_submissions_me(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let submissions = submission::get_submissions_user(&mut connection, logged_user.id)?;
+
+    render(
+        &hb,
+        "submissions",
+        &Context {
+            base,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[get("/submissions/{uuid}")]
+async fn get_submission(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    path: web::Path<(String,)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (submission_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    #[derive(Serialize)]
+    struct Submission {
+        pub uuid: String,
+        pub verdict: Option<String>,
+        pub source_text: String,
+        pub language: String,
+        pub memory_kib: Option<i32>,
+        pub time_ms: Option<i32>,
+        pub time_wall_ms: Option<i32>,
+        pub error_output: Option<String>,
+        pub user_name: String,
+        pub problem_label: String,
+        pub contest_name: String,
+        pub failed_test: Option<i32>,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        submission: Submission,
+    }
+    let (submission, user, contest_problem, contest) =
+        submission::get_submission_by_uuid(&mut connection, submission_uuid)?;
+
+    if user.id != logged_user.id && !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Não é possível acessar uma submissão de outro usuário".into(),
+        ));
+    }
+
+    render(
+        &hb,
+        "submission",
+        &Context {
+            base,
+            submission: Submission {
+                uuid: submission.uuid,
+                verdict: submission.verdict,
+                source_text: submission.source_text,
+                language: submission.language,
+                memory_kib: submission.memory_kib,
+                time_ms: submission.time_ms,
+                time_wall_ms: submission.time_wall_ms,
+                error_output: submission.error_output,
+                user_name: user.name,
+                problem_label: contest_problem.label,
+                contest_name: contest.name,
+                failed_test: submission.failed_test,
+            },
+        },
+    )
+}
+
+#[get("/submissions/me/contests/{id}")]
+async fn get_submissions_me_by_contest_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    path: web::Path<(i32,)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let submissions = submission::get_submissions_user_by_contest(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+
+    render(
+        &hb,
+        "submissions",
+        &Context {
+            base,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[get("/submissions/me/contests/{id}/{label}")]
+async fn get_submissions_me_by_contest_id_problem_label(
+    base: BaseContext,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    path: web::Path<(i32, String)>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let (contest_id, problem_label) = path.into_inner();
+    let submissions = submission::get_submissions_user_by_contest_problem(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+        &problem_label,
+    )?;
+
+    render(
+        &hb,
+        "submissions",
+        &Context {
+            base,
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
+
+#[derive(Serialize, Deserialize)]
+struct SubmissionForm {
+    contest_problem_id: i32,
+    language: String,
+    source_text: String,
+}
+
+fn redirect_to_root() -> HttpResponse {
+    HttpResponse::SeeOther()
+        .append_header((
+            header::LOCATION,
+            HeaderValue::from_str(
+                &env::var("BASE_URL").expect("BASE_URL environment variable is not set"),
+            )
+            .unwrap(),
+        ))
+        .finish()
+}
+
+fn redirect_to_referer(message: String, request: &HttpRequest) -> HttpResponse {
+    let referer = request
+        .headers()
+        .get("Referer")
+        .and_then(|h| h.to_str().ok())
+        .map(|s| s.into())
+        .unwrap_or(env::var("BASE_URL").expect("BASE_URL environment variable is not set"));
+    FlashMessage::info(message).send();
+    HttpResponse::SeeOther()
+        .append_header((header::LOCATION, HeaderValue::from_str(&referer).unwrap()))
+        .finish()
+}
+
+#[derive(Serialize, Deserialize)]
+struct ChangePasswordForm {
+    old_password: String,
+    new_password: String,
+    new_password_repeat: String,
+}
+
+#[post("/me/password")]
+async fn change_password(
+    identity: Identity,
+    form: web::Form<ChangePasswordForm>,
+    pool: web::Data<DbPool>,
+    request: HttpRequest,
+) -> PageResult {
+    let identity = require_identity(&identity)?;
+    if form.new_password != form.new_password_repeat {
+        return Err(PageError::Validation("Senhas são diferentes".into()));
+    }
+
+    let mut connection = pool.get()?;
+
+    match user::change_password(
+        &mut connection,
+        identity.id,
+        &form.old_password,
+        &form.new_password,
+    )? {
+        PasswordMatched::PasswordMatches(_) => Ok(redirect_to_referer(
+            "Senha alterada com sucesso".into(),
+            &request,
+        )),
+        _ => Ok(redirect_to_referer(
+            "Senha antiga incorreta".into(),
+            &request,
+        )),
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct CreateUserForm {
+    name: String,
+    password: String,
+    is_admin: Option<bool>,
+}
+
+#[post("/users/")]
+async fn create_user(
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    form: web::Form<CreateUserForm>,
+    request: HttpRequest,
+) -> PageResult {
+    let identity = require_identity(&identity)?;
+    if !identity.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+
+    let mut connection = pool.get()?;
+
+    user::insert_new_user(
+        &mut connection,
+        user::NewUser {
+            name: &form.name,
+            password: &form.password,
+            is_admin: form.is_admin.unwrap_or(false),
+            creation_instant: Local::now().naive_utc(),
+            creation_user_id: Some(identity.id),
+        },
+    )?;
+
+    Ok(redirect_to_referer(
+        "Usuário criado com sucesso".into(),
+        &request,
+    ))
+}
+
+#[derive(Serialize, Deserialize)]
+struct ImpersonateUserForm {
+    name: String,
+}
+
+#[post("/impersonate/")]
+async fn impersonate_user(
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    form: web::Form<ImpersonateUserForm>,
+    request: HttpRequest,
+) -> PageResult {
+    let my_identity = require_identity(&identity)?;
+    if !my_identity.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+
+    let mut connection = pool.get()?;
+
+    let user = user::get_user_by_name(&mut connection, &form.name)?;
+    Identity::login(
+        &request.extensions(),
+        serde_json::to_string(&LoggedUser {
+            id: user.id,
+            name: (&user.name).into(),
+            is_admin: user.is_admin,
+        })
+        .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?,
+    )
+    .map_err(|_| PageError::Custom("Impossível fazer login".into()))?;
+
+    Ok(redirect_to_referer(
+        "Personificado com sucesso".into(),
+        &request,
+    ))
+}
+
+#[post("/submissions/")]
+async fn create_submission(
+    identity: Identity,
+    form: web::Form<SubmissionForm>,
+    pool: web::Data<DbPool>,
+    job_sender: web::Data<Sender<Job>>,
+    languages: web::Data<Arc<DashMap<String, Language>>>,
+    session: Session,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    languages
+        .get(&form.language)
+        .ok_or(PageError::Validation("Linguagem inexistente".into()))?;
+
+    if !logged_user.is_admin && form.language != "cpp.17.g++" {
+        return Err(PageError::Validation(
+            "Somente é possível submeter em C++".into(),
+        ));
+    }
+
+    let uuid = Uuid::new_v4();
+    submission::insert_submission(
+        &mut connection,
+        submission::NewSubmission {
+            uuid: uuid.to_string(),
+            source_text: (&form.source_text).into(),
+            language: (&form.language).into(),
+            submission_instant: Local::now().naive_utc(),
+            contest_problem_id: form.contest_problem_id,
+            user_id: logged_user.id,
+        },
+    )?;
+
+    let contest =
+        contest::get_contest_by_contest_problem_id(&mut connection, form.contest_problem_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+
+    let metadata =
+        problem::get_problem_by_contest_id_metadata(&mut connection, form.contest_problem_id)?;
+
+    job_sender
+        .send(Job {
+            uuid: uuid.to_string(),
+            language: (&form.language).into(),
+            time_limit_ms: metadata.time_limit_ms,
+            memory_limit_kib: metadata.memory_limit_bytes / 1_024,
+
+            which: Some(job::Which::Judgement(job::Judgement {
+                source_text: (&form.source_text).into(),
+                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(),
+            })),
+        })
+        .await?;
+
+    session.insert("language", &form.language)?;
+    Ok(redirect_to_referer(
+        format!("Submetido {} com sucesso!", uuid),
+        &request,
+    ))
+}
+
+#[post("/submissions/{uuid}/rejudge")]
+async fn rejudge_submission(
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    job_sender: web::Data<Sender<Job>>,
+    request: HttpRequest,
+    path: web::Path<(String,)>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+
+    let (submission_uuid,) = path.into_inner();
+    let (submission, _, _, _) =
+        submission::get_submission_by_uuid(&mut connection, submission_uuid.clone())?;
+    let metadata = problem::get_problem_by_contest_id_metadata(
+        &mut connection,
+        submission.contest_problem_id,
+    )?;
+
+    job_sender
+        .send(crate::create_job_from_submission(submission, metadata))
+        .await?;
+
+    Ok(redirect_to_referer(
+        format!("Rejulgando {}", submission_uuid),
+        &request,
+    ))
+}
+
+#[derive(Serialize)]
+struct FormattedContest {
+    pub id: i32,
+    pub name: String,
+    pub start_instant: Option<String>,
+    pub end_instant: Option<String>,
+    pub creation_instant: String,
+    pub grade_ratio: Option<i32>,
+    pub grade_after_ratio: Option<i32>,
+    pub accepted_count: i32,
+    pub accepted_after_count: i32,
+    pub accepted_total_count: i32,
+    pub problem_count: i32,
+    pub grade: String,
+}
+
+fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest {
+    FormattedContest {
+        id: contest.id,
+        name: contest.name.clone(),
+        start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)),
+        end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)),
+        creation_instant: format_utc_date_time(&tz, contest.creation_instant),
+        grade_ratio: contest.grade_ratio,
+        grade_after_ratio: contest.grade_after_ratio,
+        accepted_count: 0,
+        accepted_after_count: 0,
+        accepted_total_count: 0,
+        problem_count: 0,
+        grade: "".into(),
+    }
+}
+
+fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedContest {
+    FormattedContest {
+        id: contest.id,
+        name: contest.name.clone(),
+        start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)),
+        end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)),
+        creation_instant: format_utc_date_time(&tz, contest.creation_instant),
+        grade_ratio: contest.grade_ratio,
+        grade_after_ratio: contest.grade_after_ratio,
+        accepted_count: contest.accepted_count,
+        accepted_after_count: contest.accepted_after_count,
+        accepted_total_count: contest.accepted_count + contest.accepted_after_count,
+        problem_count: contest.problem_count,
+        grade: match contest.grade_ratio {
+            Some(grade_ratio) => format!(
+                "{:.2}",
+                10.0 * (f64::from(contest.accepted_count) * 1.0 / f64::from(grade_ratio)
+                    + match contest.grade_after_ratio {
+                        Some(grade_after_ratio) =>
+                            f64::from(contest.accepted_after_count) * 1.0
+                                / f64::from(grade_after_ratio),
+                        None => 0.0,
+                    })
+            )
+            .replacen(".", ",", 1),
+            None => "".into(),
+        },
+    }
+}
+
+fn get_formatted_contests(
+    connection: &mut PgConnection,
+    user_id: Option<i32>,
+    tz: &Tz,
+) -> Result<Vec<FormattedContest>, PageError> {
+    Ok(match user_id {
+        Some(user_id) => contest::get_contests_with_acs(connection, user_id)?
+            .iter()
+            .map(|c| get_formatted_contest_acs(tz, c))
+            .collect(),
+        None => contest::get_contests(connection)?
+            .iter()
+            .map(|c| get_formatted_contest(tz, c))
+            .collect(),
+    })
+}
+
+#[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,
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    hb: web::Data<Handlebars<'_>>,
+    tz: web::Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contests: Vec<FormattedContest>,
+    }
+
+    let mut connection = pool.get()?;
+    render(
+        &hb,
+        "contests",
+        &Context {
+            base,
+            contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?,
+        },
+    )
+}
+
+#[post("/contests/")]
+async fn create_contest(
+    identity: Identity,
+    pool: web::Data<DbPool>,
+    mut payload: Multipart,
+    job_sender: web::Data<Sender<Job>>,
+    job_result_sender: web::Data<broadcast::Sender<JobResult>>,
+    tz: web::Data<Tz>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+
+    #[derive(Debug)]
+    struct Form {
+        name: Option<String>,
+        start_instant: Option<String>,
+        end_instant: Option<String>,
+        polygon_zip: Option<Cursor<Vec<u8>>>,
+        grade_ratio: Option<i32>,
+        grade_after_ratio: Option<i32>,
+    }
+
+    let mut form = Form {
+        name: None,
+        start_instant: None,
+        end_instant: None,
+        polygon_zip: None,
+        grade_ratio: None,
+        grade_after_ratio: None,
+    };
+
+    while let Ok(Some(mut field)) = payload.try_next().await {
+        let mut cursor = Cursor::new(vec![]);
+        while let Some(chunk) = field.next().await {
+            let data = chunk.unwrap();
+            cursor
+                .write(&data)
+                .map_err(|_| PageError::Validation("Corpo inválido".into()))?;
+        }
+
+        cursor.set_position(0);
+
+        fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> {
+            let mut value = String::new();
+            cursor
+                .read_to_string(&mut value)
+                .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?;
+            Ok(value)
+        }
+
+        match field.content_disposition().get_name() {
+            Some("name") => form.name = Some(parse_field("name", &mut cursor)?),
+            Some("start_instant") => {
+                form.start_instant = Some(parse_field("start_instant", &mut cursor)?)
+            }
+            Some("end_instant") => {
+                form.end_instant = Some(parse_field("end_instant", &mut cursor)?)
+            }
+            Some("grade_ratio") => {
+                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()
+            }
+            Some("polygon_zip") => form.polygon_zip = Some(cursor),
+            _ => {}
+        }
+    }
+
+    let polygon_zip = form
+        .polygon_zip
+        .ok_or(PageError::Validation("Arquivo não informado".into()))?;
+    let imported = import_contest::import_file(polygon_zip)
+        .map_err(|e| PageError::Validation(format!("Não foi possível importar: {}", e)))?;
+    let mut connection = pool.get()?;
+
+    let contest = if form.name.as_ref().unwrap() != "" {
+        Some(contest::insert_contest(
+            &mut connection,
+            contest::NewContest {
+                name: form.name.clone().unwrap(),
+                start_instant: form
+                    .start_instant
+                    .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+                    .map(|d| d.naive_utc()),
+                end_instant: form
+                    .end_instant
+                    .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+                    .map(|d| d.naive_utc()),
+                creation_instant: Local::now().naive_utc(),
+                creation_user_id: logged_user.id,
+                grade_ratio: form.grade_ratio,
+                grade_after_ratio: form.grade_after_ratio,
+            },
+        )?)
+    } else {
+        None
+    };
+
+    fn polygon_url_to_id_without_revision(url: String) -> String {
+        url.replace("https://polygon.codeforces.com/", "polygon.")
+            .replace("/", ".")
+    }
+
+    let problem_label: HashMap<String, String> =
+        HashMap::from_iter(imported.0.problems.problem.iter().map(|problem| {
+            (
+                polygon_url_to_id_without_revision(problem.url.clone()),
+                problem.index.clone(),
+            )
+        }));
+
+    let mut zip = imported.2;
+
+    lazy_static! {
+        static ref CODEFORCES_LANGUAGE_TO_JUGHISTO: HashMap<String, String> = {
+            let mut m = HashMap::new();
+            m.insert("cpp.g++17".into(), "cpp.17.g++".into());
+            m.insert("cpp.msys2-mingw64-9-g++17".into(), "cpp.17.g++".into());
+            m.insert("java.8".into(), "java.8".into());
+            m.insert("testlib".into(), "cpp.17.g++".into());
+            m
+        };
+    }
+
+    for (name, metadata) in imported.1 {
+        let problem_id_without_revision = polygon_url_to_id_without_revision(metadata.url);
+        let problem_id = format!("{}.r{}", problem_id_without_revision, &metadata.revision);
+
+        let files_regex: Regex = Regex::new(&format!(
+            concat!(
+                "^{}/(",
+                r"files/$|",
+                r"files/.*\.cpp$|",
+                r"files/.*\.h$|",
+                r"files/tests/$|",
+                r"files/tests/validator-tests/$|",
+                r"files/tests/validator-tests/.*$|",
+                r"files/tests/validator-tests/.*$|",
+                r"solutions/$|",
+                r"solutions/.*.cc$|",
+                r"solutions/.*.cpp$|",
+                r"statements/$|",
+                r"statements/.html/.*$|",
+                r"tests/$",
+                ")"
+            ),
+            name
+        ))
+        .unwrap();
+        let mut filenames = zip
+            .file_names()
+            .filter(|name| files_regex.is_match(name))
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>();
+        filenames.sort();
+        for name in filenames {
+            let relative_path = files_regex
+                .captures(&name)
+                .unwrap()
+                .get(1)
+                .unwrap()
+                .as_str();
+            let data_path = format!("/data/{}/{}", problem_id, relative_path);
+
+            if name.ends_with("/") {
+                info!("Creating directory {} into {}", name, data_path);
+                create_dir_all(data_path)?;
+                continue;
+            }
+
+            info!("Putting file {} into {}", name, data_path);
+            std::io::copy(&mut zip.by_name(&name)?, &mut File::create(data_path)?)?;
+        }
+
+        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))
+                })?
+                .into())
+        }
+
+        let main_solution = &metadata
+            .assets
+            .solutions
+            .solution
+            .iter()
+            .find(|s| s.tag == "main")
+            .ok_or(PageError::Validation("No main solution".into()))?
+            .source;
+
+        let problem = problem::upsert_problem(
+            &mut connection,
+            problem::NewProblem {
+                id: problem_id.clone(),
+                name: metadata.names.name[0].value.clone(),
+                memory_limit_bytes: metadata.judging.testset[0]
+                    .memory_limit
+                    .value
+                    .parse()
+                    .unwrap(),
+                time_limit_ms: metadata.judging.testset[0]
+                    .time_limit
+                    .value
+                    .parse()
+                    .unwrap(),
+                checker_path: metadata.assets.checker.source.path.clone(),
+                checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?,
+                validator_path: metadata.assets.validators.validator[0].source.path.clone(),
+                validator_language: map_codeforces_language(
+                    &metadata.assets.validators.validator[0].source.r#type,
+                )?,
+                main_solution_path: main_solution.path.clone(),
+                main_solution_language: map_codeforces_language(&main_solution.r#type)?,
+                test_pattern: metadata.judging.testset[0].input_path_pattern.value.clone(),
+                test_count: metadata.judging.testset[0]
+                    .test_count
+                    .value
+                    .parse()
+                    .unwrap(),
+                status: "compiled".into(),
+                creation_instant: Local::now().naive_utc(),
+                creation_user_id: logged_user.id,
+            },
+        )?;
+
+        for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() {
+            let i = i + 1;
+            let test_path = format!(
+                "./{}/{}",
+                problem_id,
+                import_contest::format_width(&problem.test_pattern, i)
+            );
+
+            info!(
+                "Iterating through test {} to {:#?}, which is {}",
+                i,
+                test_path,
+                test.method.as_ref().unwrap()
+            );
+            if test.method.as_ref().unwrap() == "manual" {
+                let test_name = PathBuf::from(&name)
+                    .join(import_contest::format_width(&problem.test_pattern, i));
+                info!("Extracting {:#?} from zip", test_name);
+                std::io::copy(
+                    &mut zip.by_name(&test_name.to_str().unwrap())?,
+                    &mut File::create(PathBuf::from("/data/").join(&test_path))?,
+                )?;
+            } else {
+                let cmd: Vec<_> = test.cmd.as_ref().unwrap().split(" ").collect();
+                let run_stats = language::run_cached(
+                    &job_sender,
+                    &job_result_sender,
+                    &"cpp.17.g++".into(),
+                    format!("./{}/files/{}.cpp", problem.id, cmd.get(0).unwrap()),
+                    cmd[1..].iter().map(|s| s.clone().into()).collect(),
+                    None,
+                    Some(test_path.clone()),
+                    problem.memory_limit_bytes / 1_024,
+                    10_000,
+                )
+                .await
+                .map_err(|_| {
+                    PageError::Validation("Couldn't use an intermediate program".into())
+                })?;
+
+                if run_stats.result != i32::from(job_result::run_cached::Result::Ok) {
+                    return Err(PageError::Validation(
+                        "Couldn't run an intermediate program".into(),
+                    ));
+                }
+            }
+
+            let run_stats = language::run_cached(
+                &job_sender,
+                &job_result_sender,
+                &problem.main_solution_language,
+                format!("./{}/{}", problem.id, problem.main_solution_path),
+                vec![],
+                Some(test_path.clone()),
+                Some(format!("{}.a", test_path)),
+                problem.memory_limit_bytes / 1_024,
+                problem.time_limit_ms,
+            )
+            .await
+            .map_err(|_| PageError::Validation("Couldn't run solution on test".into()))?;
+            if run_stats.exit_code != 0 {
+                return Err(PageError::Validation(
+                    "Couldn't run solution on test".into(),
+                ));
+            }
+        }
+
+        language::judge(
+            &job_sender,
+            &job_result_sender,
+            &problem.main_solution_language,
+            fs::read_to_string(PathBuf::from(format!(
+                "/data/{}/{}",
+                problem.id, problem.main_solution_path
+            )))?,
+            problem.test_count,
+            format!("./{}/{}", problem.id, problem.test_pattern).into(),
+            problem.checker_language,
+            format!("./{}/{}", problem.id, problem.checker_path).into(),
+            problem.memory_limit_bytes / 1_024,
+            problem.time_limit_ms,
+        )
+        .await
+        .map_err(|_| PageError::Validation("Couldn't judge main solution".into()))?;
+
+        if form.name.as_ref().unwrap() != "" {
+            contest::relate_problem(
+                &mut connection,
+                contest::NewContestProblems {
+                    label: problem_label
+                        .get(&problem_id_without_revision)
+                        .ok_or(PageError::Validation(
+                            "Arquivo não contém problemas listados".into(),
+                        ))?
+                        .to_string()
+                        .to_uppercase(),
+                    contest_id: contest.as_ref().unwrap().id,
+                    problem_id,
+                },
+            )?;
+        }
+    }
+
+    if form.name.as_ref().unwrap() != "" {
+        Ok(redirect_to_referer(
+            "Competição criada com sucesso".into(),
+            &request,
+        ))
+    } else {
+        Ok(redirect_to_referer(
+            "Problemas adicionados com sucesso".into(),
+            &request,
+        ))
+    }
+}