features, cleanup and bug fixes #26

Merged
ben merged 30 commits from ben/Jobboerse:main into main 2022-06-09 17:36:55 +02:00
10 changed files with 225 additions and 214 deletions
Showing only changes of commit d98df7839d - Show all commits

reactor Error types

better separate response error types used for route handlers and other errors
- with an exception of PresentationError
Bennet Bleßmann 2022-05-30 21:10:05 +02:00 committed by Bennet Bleßmann
Signed by: ben
GPG key ID: 3BE1A1A3CBC3CF99

View file

@ -42,11 +42,6 @@ impl Display for LoginRequired {
}
}
impl Error for LoginRequired {}
impl ResponseError for LoginRequired {
fn status_code(&self) -> StatusCode {
StatusCode::UNAUTHORIZED
}
}
#[derive(Debug, thiserror::Error)]
pub enum MultipartFieldError {
@ -83,39 +78,18 @@ pub enum MultipartFieldError {
Runtime(#[from] tokio::task::JoinError),
}
impl ResponseError for MultipartFieldError {
fn status_code(&self) -> StatusCode {
match self {
MultipartFieldError::IOError(_) | MultipartFieldError::Runtime(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
MultipartFieldError::ContentTooLarge { .. }
| MultipartFieldError::Date { .. }
| MultipartFieldError::MultipartError(_)
| MultipartFieldError::NotAFile { field: _ }
| MultipartFieldError::TooManyOccurrences { .. }
| MultipartFieldError::UTF8Error(_)
| MultipartFieldError::MissingField { field: _ }
| MultipartFieldError::InvalidAddress(_) => StatusCode::BAD_REQUEST,
}
}
}
#[derive(Error, Debug)]
pub(crate) enum PresentationError {
#[error("Failed to render page template: {0}")]
Render(#[from] RenderError),
#[error("Failed to generate URL for route: {0}")]
Url(#[from] UrlGenerationError),
#[error("Couldn't generate response as login is required")]
LoginRequired(#[from] LoginRequired),
}
impl ResponseError for PresentationError {
fn status_code(&self) -> StatusCode {
match self {
Self::Render(_) | Self::Url(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::LoginRequired(_) => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
@ -185,8 +159,8 @@ mod ldap_response_code {
pub const UNAVAILABLE: u32 = 52;
}
impl ResponseError for AuthenticationError {
fn status_code(&self) -> StatusCode {
impl AuthenticationError {
pub(crate) fn as_status_code(&self) -> StatusCode {
match self {
AuthenticationError::Ldap(ldap_error) => match ldap_error {
LdapError::LdapResult { result } => match result.rc {
@ -203,3 +177,16 @@ impl ResponseError for AuthenticationError {
}
}
}
#[derive(Debug, Error)]
#[error("Authentication failed: {0}")]
pub(crate) struct LoginResponseError(#[from] AuthenticationError);
impl ResponseError for LoginResponseError {
fn status_code(&self) -> StatusCode {
self.0.as_status_code()
}
fn error_response(&self) -> HttpResponse<BoxBody> {
default_error_response(self, self.status_code())
}
}

View file

@ -11,7 +11,7 @@ use std::time::Duration;
use actix_web::error::UrlGenerationError;
use actix_web::{HttpRequest, Result};
use chrono::{FixedOffset, TimeZone, Timelike};
use error::{DeleteError, SaveError};
use error::DeleteError;
use lettre::Address;
use log::{debug, error, info, warn};
use rand::distributions::DistString;
@ -22,6 +22,7 @@ use toml::value::Datetime;
use crate::auth::User;
use crate::error::PresentationError;
use crate::job_offers::error::SaveError;
use crate::route::{JOBOFFER_ATTACHMENT_ROUTE, PREVIEW_ATTACHMENT_ROUTE};
use crate::route::{JOBOFFER_DELETION_ROUTE, JOBOFFER_PUBLISH_ROUTE, JOBOFFER_UNPUBLISH_ROUTE};
use crate::util::{toml_date_to_chrono_date, toml_datetime_to_chrono_datetime};

View file

@ -6,26 +6,20 @@ use http::StatusCode;
#[derive(thiserror::Error, Debug)]
pub(crate) enum SaveError {
#[error("Creating a new Job Offer failed as the generated ID is already taken")]
AlreadyExists,
#[error("{0}")]
IO(#[from] std::io::Error),
#[error("{0}")]
Persist(#[from] tempfile::PersistError),
#[error("Could not serialize Job Offer: {0}")]
Serialize(#[from] toml::ser::Error),
#[error("Could not generate url for confirmation link: {0}")]
Url(#[from] UrlGenerationError),
#[error("Could not send Confirmation E-Mail: {0}")]
Email(#[from] EmailError),
#[error("The Runtime encountered an error!")]
#[error("The Runtime encountered an error!: {0}")]
Runtime(#[from] tokio::task::JoinError),
#[error("A reviewer-only setting was specified, but no valid session was found.")]
LoginRequired(#[from] LoginRequired),
#[error("Creating a new Job Offer failed as the generated ID is already taken")]
AlreadyExists,
}
impl ResponseError for SaveError {
fn status_code(&self) -> StatusCode {
impl SaveError {
pub(crate) fn as_status_code(&self) -> StatusCode {
match self {
// add special handling of ErrorKind::StorageFull | FilesystemQuotaExceeded once those error kinds are stabilized
/*
@ -42,14 +36,35 @@ impl ResponseError for SaveError {
{
StatusCode::INSUFFICIENT_STORAGE
}*/
SaveError::Persist(_)
| SaveError::IO(_)
SaveError::IO(_)
| SaveError::Persist(_)
| SaveError::Serialize(_)
| SaveError::Url(_)
| SaveError::Runtime(_) => StatusCode::INTERNAL_SERVER_ERROR,
SaveError::AlreadyExists => StatusCode::CONFLICT,
SaveError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR,
SaveError::LoginRequired(_) => StatusCode::UNAUTHORIZED,
}
}
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum SaveResponseError {
#[error("Could not generate url for confirmation link: {0}")]
Url(#[from] UrlGenerationError),
#[error("Could not save job offer: {0}")]
Save(#[from] SaveError),
#[error("Could not send Confirmation E-Mail: {0}")]
Email(#[from] EmailError),
#[error("A reviewer-only setting was specified, but no valid session was found.")]
Login(#[from] LoginRequired),
}
impl ResponseError for SaveResponseError {
fn status_code(&self) -> StatusCode {
match self {
SaveResponseError::Save(inner) => inner.as_status_code(),
SaveResponseError::Url(_) | SaveResponseError::Email(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
SaveResponseError::Login(_) => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
@ -72,17 +87,3 @@ pub(crate) enum DeleteError {
#[error("{0}")]
IO(#[from] std::io::Error),
}
impl ResponseError for DeleteError {
fn status_code(&self) -> StatusCode {
match self {
DeleteError::IO(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let status_code = self.status_code();
match self {
DeleteError::IO(inner) => default_error_response(inner, status_code),
}
}
}

View file

@ -6,7 +6,7 @@ use log::{error, trace};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::error::{AuthenticationError, PresentationError};
use crate::error::{LoginResponseError, PresentationError};
use crate::route::HTML_CONTENT;
use crate::{template, ServerConfig};
@ -73,7 +73,7 @@ pub(crate) async fn login_post(
query: web::Query<LoginQuery>,
config: web::Data<ServerConfig>,
session: Session,
) -> Result<HttpResponse, AuthenticationError> {
) -> Result<HttpResponse, LoginResponseError> {
let result = crate::auth::User::login(&form.username, &form.password, &session, &config).await;
match result {
@ -99,7 +99,7 @@ pub(crate) async fn login_post(
}
Err(err) => {
error!("failed to perform authentication for login: {}", err);
Err(err)
Err(err.into())
}
}
}

View file

@ -1,10 +1,12 @@
use crate::auth::User;
use crate::error::{AuthenticationError, LoginRequired, MultipartFieldError, PresentationError};
use crate::job_offers::error::{DeleteError, SaveError};
use crate::error::{MultipartFieldError, PresentationError};
use crate::job_offers::error::SaveResponseError;
use crate::route::LOGIN_ROUTE;
use crate::{route, ServerConfig};
use crate::route::job_offer::error::{ConfirmationError, DeletionError, SubmissionError};
use crate::route::job_offer::error::{
ConfirmationResponseError, DeletionResponseError, SubmissionResponseError, SyncResponseError,
};
use actix_session::SessionExt;
use actix_web::dev::ServiceResponse;
use actix_web::error::UrlGenerationError;
@ -22,9 +24,9 @@ use url::Url;
#[derive(Debug, thiserror::Error)]
#[error("Some error occurred while attempting to display an error page")]
pub struct ErrorHandlerError;
pub struct ErrorHandlerResponseError;
impl ResponseError for ErrorHandlerError {}
impl ResponseError for ErrorHandlerResponseError {}
pub(crate) fn generic_server_error_handler<B>(
res: ServiceResponse,
@ -32,8 +34,8 @@ pub(crate) fn generic_server_error_handler<B>(
title: &str,
msg: Option<&str>,
) -> Result<ErrorHandlerResponse<B>, actix_web::Error> {
let hb: &Data<Handlebars> = res.request().app_data().ok_or(ErrorHandlerError)?;
let config: &Data<ServerConfig> = res.request().app_data().ok_or(ErrorHandlerError)?;
let hb: &Data<Handlebars> = res.request().app_data().ok_or(ErrorHandlerResponseError)?;
let config: &Data<ServerConfig> = res.request().app_data().ok_or(ErrorHandlerResponseError)?;
let base = route::base(res.request(), config, title)?;
let session = res.get_session();
@ -61,19 +63,15 @@ pub(crate) fn internal_server_error_handler<B>(
res: ServiceResponse,
) -> Result<ErrorHandlerResponse<B>, actix_web::Error> {
if let Some(err) = res.response().error() {
if let Some(err) = err.as_error::<SaveError>() {
if let Some(err) = err.as_error::<SaveResponseError>() {
error!("Internal Server Error due to SaveError: {}", err)
} else if let Some(err) = err.as_error::<DeleteError>() {
} else if let Some(err) = err.as_error::<DeletionResponseError>() {
error!("Internal Server Error due to DeleteError: {}", err)
} else if let Some(err) = err.as_error::<ErrorHandlerError>() {
} else if let Some(err) = err.as_error::<ErrorHandlerResponseError>() {
error!("Internal Server Error due to ErrorHandlerError: {}", err)
} else if let Some(err) = err.as_error::<MultipartFieldError>() {
error!("Internal Server Error due to MultipartFieldError: {}", err)
} else if let Some(err) = err.as_error::<PresentationError>() {
error!("Internal Server Error due to PresentationError: {}", err)
} else if let Some(err) = err.as_error::<AuthenticationError>() {
error!("Internal Server Error due to AuthenticationError: {}", err)
} else if let Some(err) = err.as_error::<SubmissionError>() {
} else if let Some(err) = err.as_error::<SubmissionResponseError>() {
error!("Internal Server Error due to SubmissionError: {}", err)
} else {
error!("Unknown Error Type for Internal Server Error")
@ -100,15 +98,15 @@ pub(crate) fn bad_request<B>(
) -> Result<ErrorHandlerResponse<B>, actix_web::Error> {
let msg;
let msg = if let Some(err) = res.response().error() {
if let Some(err) = err.as_error::<SubmissionError>() {
if let Some(err) = err.as_error::<SubmissionResponseError>() {
match err {
SubmissionError::MissingLinkOrAttachment => {
SubmissionResponseError::MissingLinkOrAttachment => {
Some("Es muss mindestens ein Link oder ein Anhang angegeben werden.")
}
SubmissionError::Form(MultipartFieldError::Date { err:_ }) => {
SubmissionResponseError::Form(MultipartFieldError::Date { err:_ }) => {
Some("Eine Datumsangabe entsprach nicht dem erwarteten Format.")
}
SubmissionError::Form(MultipartFieldError::ContentTooLarge {
SubmissionResponseError::Form(MultipartFieldError::ContentTooLarge {
field,
max_byte_size,
}) => {
@ -117,32 +115,32 @@ pub(crate) fn bad_request<B>(
);
Some(msg.as_str())
}
SubmissionError::Form(MultipartFieldError::TooManyOccurrences { field, limit }) => {
SubmissionResponseError::Form(MultipartFieldError::TooManyOccurrences { field, limit }) => {
msg = format!(
"Das Feld mit der ID {field} wurde zu oft vorhanden, erlaubt sind {limit} vorkommen."
);
Some(msg.as_str())
}
SubmissionError::Form(MultipartFieldError::MultipartError(mpe)) => {
SubmissionResponseError::Form(MultipartFieldError::MultipartError(mpe)) => {
warn!("{}", mpe);
msg = format!("{}", mpe);
Some(msg.as_str())
}
SubmissionError::Form(MultipartFieldError::NotAFile { field }) => {
SubmissionResponseError::Form(MultipartFieldError::NotAFile { field }) => {
msg = format!(
"Das Feld mit der ID {field} erwartet eine Datei, aber der zugehörige ContentDisposition-Header enthielt keinen Dateinamen."
);
Some(msg.as_str())
}
SubmissionError::Form(MultipartFieldError::MissingField { field }) => {
SubmissionResponseError::Form(MultipartFieldError::MissingField { field }) => {
msg =
format!("Das Feld mit der ID {field} fehlt obwohl es nicht optional ist.");
Some(msg.as_str())
}
SubmissionError::Form(MultipartFieldError::UTF8Error(_err)) => {
SubmissionResponseError::Form(MultipartFieldError::UTF8Error(_err)) => {
Some("Ein Feld das im UTF-8 format erwartet wurde konnte nicht als UTF-8 geparst werden.")
}
SubmissionError::Form(MultipartFieldError::InvalidAddress(reason)) => {
SubmissionResponseError::Form(MultipartFieldError::InvalidAddress(reason)) => {
Some(match reason {
AddressError::MissingParts => { "Unvollständige E-Mail Address" }
AddressError::Unbalanced => { "Unausgeglichene Klammern '<' & '>' in E-Mail Address. " }
@ -150,11 +148,11 @@ pub(crate) fn bad_request<B>(
AddressError::InvalidDomain => { "Der Host/Domain Teil (nach dem @) der E-Mail Address ist ungültig." }
})
}
SubmissionError::Save(_)
| SubmissionError::TooManyRequests
| SubmissionError::Render(_)
| SubmissionError::Form(MultipartFieldError::IOError(_))
| SubmissionError::Form(MultipartFieldError::Runtime(_)) => {
SubmissionResponseError::Save(_)
| SubmissionResponseError::TooManyRequests
| SubmissionResponseError::Render(_)
| SubmissionResponseError::Form(MultipartFieldError::IOError(_))
| SubmissionResponseError::Form(MultipartFieldError::Runtime(_)) => {
error!(
"Response Status Code (Bad Request) and Error appear to disagree : {}",
err
@ -162,17 +160,17 @@ pub(crate) fn bad_request<B>(
None
}
}
} else if let Some(err) = err.as_error::<ConfirmationError>() {
} else if let Some(err) = err.as_error::<ConfirmationResponseError>() {
match err {
ConfirmationError::InvalidRequest => {
ConfirmationResponseError::InvalidRequest => {
Some("Die Stellenanzeige erwartet keine Bestätigung, die Stellenanzeigen ID ist ungültig oder der Bestätigungstoken is ungültig.")
}
ConfirmationError::Save(_)
| ConfirmationError::Delete(_)
| ConfirmationError::SuccessRenderError(_)
| ConfirmationError::RenderError(_)
| ConfirmationError::Url(_)
| ConfirmationError::Presentation(_) => {
ConfirmationResponseError::Save(_)
| ConfirmationResponseError::Delete(_)
| ConfirmationResponseError::SuccessRenderError(_)
| ConfirmationResponseError::RenderError(_)
| ConfirmationResponseError::Url(_)
| ConfirmationResponseError::Presentation(_) => {
error!(
"Response Status Code (Bad Request) and Error appear to disagree : {}",
err
@ -180,17 +178,17 @@ pub(crate) fn bad_request<B>(
None
}
}
} else if let Some(err) = err.as_error::<MultipartFieldError>() {
warn!("Unexpected MultipartFieldError as top level error!");
msg = err.to_string();
Some(msg.as_str())
} else if let Some(err) = err.as_error::<DeletionError>() {
} else if let Some(err) = err.as_error::<DeletionResponseError>() {
match err {
DeletionError::Delete(err) => {
DeletionResponseError::Presentation(err) => {
warn!("Failed to present deletion response: {}", err);
Some("Failed to generate response.")
}
DeletionResponseError::Delete(err) => {
warn!("Couldn't delete Job Offer: {}", err);
Some("Could not delete a Job Offer")
}
DeletionError::Login(_) => {
DeletionResponseError::Login(_) => {
error!(
"Response Status Code (Bad Request) and Error appear to disagree : {}",
err
@ -226,23 +224,22 @@ pub(crate) fn unauthorized_error_handler<B>(
.response()
.error()
.and_then(|err| {
err.as_error::<LoginRequired>()
.or_else(|| {
err.as_error::<SaveError>().and_then(|elem| match elem {
SaveError::LoginRequired(login) => Some(login),
_ => None,
})
err.as_error::<SaveResponseError>()
.and_then(|elem| match elem {
SaveResponseError::Login(login) => Some(login),
_ => None,
})
.or_else(|| {
err.as_error::<DeletionError>().and_then(|elem| match elem {
DeletionError::Login(login) => Some(login),
_ => None,
})
})
.or_else(|| {
err.as_error::<PresentationError>()
err.as_error::<DeletionResponseError>()
.and_then(|elem| match elem {
PresentationError::LoginRequired(login) => Some(login),
DeletionResponseError::Login(login) => Some(login),
_ => None,
})
})
.or_else(|| {
err.as_error::<SyncResponseError>()
.and_then(|elem| match elem {
SyncResponseError::Login(login) => Some(login),
_ => None,
})
})

View file

@ -3,7 +3,7 @@ use actix_session::Session;
use actix_web::{
get, http::header, post, web, web::ServiceConfig, HttpRequest, HttpResponse, Responder, Result,
};
use error::AttachmentError;
use error::AttachmentResponseError;
use handlebars::Handlebars;
use http::header::CONTENT_TYPE;
use serde::Deserialize;
@ -17,7 +17,7 @@ pub(crate) mod error;
use crate::auth::User;
use crate::error::PresentationError;
use crate::job_offers::JobOffers;
use crate::route::job_offer::error::SyncError;
use crate::route::job_offer::error::SyncResponseError;
use crate::route::{HTML_CONTENT, JSON_CONTENT};
use crate::server_config::ServerConfig;
use crate::template;
@ -93,14 +93,14 @@ pub(crate) async fn job_offer_attachment(
config: web::Data<ServerConfig>,
session: Session,
offers: web::Data<JobOffers>,
) -> Result<impl Responder, AttachmentError> {
) -> Result<impl Responder, AttachmentResponseError> {
let id = &path.id;
let attachment_name = path.attachment.as_str();
let offer = offers
.get_offer(id)
.await
.ok_or(AttachmentError::OfferNotFound)?;
.ok_or(AttachmentResponseError::OfferNotFound)?;
if !offer.is_published()
&& !query
@ -174,7 +174,7 @@ pub(crate) async fn sync(
session: Session,
config: web::Data<ServerConfig>,
offers: web::Data<JobOffers>,
) -> Result<HttpResponse, SyncError> {
) -> Result<HttpResponse, SyncResponseError> {
// TODO return the user to a page where they are asked to confirm syncing
// aka. the get variant of this route
User::current(&session)?;

View file

@ -7,7 +7,7 @@ use serde_json::json;
use crate::auth::User;
use crate::error::PresentationError;
use crate::route::job_offer::error::{DeletionError, StateChangeError};
use crate::route::job_offer::error::{DeletionResponseError, StateChangeResponseError};
use crate::route::{HTML_CONTENT, JOBOFFER_OVERVIEW_ROUTE};
use crate::{auth, template, JobOffers, ServerConfig};
@ -20,7 +20,7 @@ pub(crate) async fn delete_joboffer(
offers: web::Data<JobOffers>,
config: web::Data<ServerConfig>,
session: Session,
) -> actix_web::Result<HttpResponse, DeletionError> {
) -> actix_web::Result<HttpResponse, DeletionResponseError> {
// TODO return the user to a page where they are asked to confirm deletion
// aka. the get variant of this route
let _user = auth::User::current(&session)?;
@ -47,7 +47,7 @@ pub(crate) async fn delete_expired_joboffers(
offers: web::Data<JobOffers>,
config: web::Data<ServerConfig>,
session: Session,
) -> actix_web::Result<HttpResponse, PresentationError> {
) -> actix_web::Result<HttpResponse, DeletionResponseError> {
let user = User::current(&session)?;
let offers_guard = offers.get_offers().await;
@ -57,14 +57,17 @@ pub(crate) async fn delete_expired_joboffers(
Some(&user),
);
let base = crate::route::base(&req, &config, "Delete Expired")?;
let base =
crate::route::base(&req, &config, "Delete Expired").map_err(PresentationError::Url)?;
let data = json! {{
"base": base,
"expired_job_offers": job_offers,
}};
let rendered = hb.render(template::JOBOFFER_DELETE_EXPIRED, &data)?;
let rendered = hb
.render(template::JOBOFFER_DELETE_EXPIRED, &data)
.map_err(PresentationError::Render)?;
Ok(HttpResponse::Ok()
.insert_header((http::header::CONTENT_TYPE, HTML_CONTENT.clone()))
@ -127,7 +130,7 @@ pub(crate) async fn bulk_delete(
config: web::Data<ServerConfig>,
form: web::Form<BulkDeleteData>,
session: Session,
) -> actix_web::Result<HttpResponse, DeletionError> {
) -> actix_web::Result<HttpResponse, DeletionResponseError> {
debug!("Received bulk deletion request!");
let _user = User::current(&session)?;
@ -154,7 +157,7 @@ pub(crate) async fn review_joboffer(
session: Session,
config: web::Data<ServerConfig>,
offers: web::Data<JobOffers>,
) -> actix_web::Result<HttpResponse, StateChangeError> {
) -> actix_web::Result<HttpResponse, StateChangeResponseError> {
// TODO return the user to a page where they are asked to confirm publishing
// aka. the get variant of this route
let _user = auth::User::current(&session)?;
@ -182,7 +185,7 @@ pub(crate) async fn unpublish_joboffer(
session: Session,
config: web::Data<ServerConfig>,
offers: web::Data<JobOffers>,
) -> actix_web::Result<HttpResponse, StateChangeError> {
) -> actix_web::Result<HttpResponse, StateChangeResponseError> {
// TODO return the user to a page where they are asked to confirm un-publishing
// aka. the get variant of this route
let _user = auth::User::current(&session)?;

View file

@ -6,8 +6,8 @@ use serde_json::json;
use crate::auth::User;
use crate::job_offers::JobOfferData;
use crate::route::job_offer::error::ConfirmationError;
use crate::route::job_offer::error::ConfirmationError::SuccessRenderError;
use crate::route::job_offer::error::ConfirmationResponseError;
use crate::route::job_offer::error::ConfirmationResponseError::SuccessRenderError;
use crate::route::HTML_CONTENT;
use crate::{get, template, JobOffers, ServerConfig};
@ -35,13 +35,13 @@ pub(crate) async fn confirm_joboffer_get(
config: web::Data<ServerConfig>,
offers: web::Data<JobOffers>,
path: web::Path<(String, String)>,
) -> actix_web::Result<HttpResponse, ConfirmationError> {
) -> actix_web::Result<HttpResponse, ConfirmationResponseError> {
let id: &String = &path.0;
let req_token = &path.1;
if let Some(job_offer) = offers.get_offer(id).await {
if !job_offer.check_confirmation_token(req_token) {
Err(ConfirmationError::InvalidRequest)
Err(ConfirmationResponseError::InvalidRequest)
} else {
let user = User::current(&session).ok();
@ -66,14 +66,14 @@ pub(crate) async fn confirm_joboffer_get(
let body = hb
.render(template::JOBOFFER_CONFIRM_SUBMISSION, &data)
.map_err(ConfirmationError::RenderError)?;
.map_err(ConfirmationResponseError::RenderError)?;
Ok(HttpResponse::Ok()
.insert_header((http::header::CONTENT_TYPE, HTML_CONTENT.clone()))
.body(body))
}
} else {
Err(ConfirmationError::InvalidRequest)
Err(ConfirmationResponseError::InvalidRequest)
}
}
@ -88,7 +88,7 @@ pub(crate) async fn confirm_joboffer_post(
path: web::Path<(String, String)>,
offers: web::Data<JobOffers>,
config: web::Data<ServerConfig>,
) -> actix_web::Result<HttpResponse, ConfirmationError> {
) -> actix_web::Result<HttpResponse, ConfirmationResponseError> {
let id = &path.0;
let req_token = &path.1;
@ -113,10 +113,10 @@ pub(crate) async fn confirm_joboffer_post(
.insert_header((http::header::CONTENT_TYPE, HTML_CONTENT.clone()))
.body(body))
}
Err(()) => Err(ConfirmationError::InvalidRequest),
Err(()) => Err(ConfirmationResponseError::InvalidRequest),
}
} else {
Err(ConfirmationError::InvalidRequest)
Err(ConfirmationResponseError::InvalidRequest)
}
}
@ -130,7 +130,7 @@ pub(crate) async fn reject_joboffer_post(
hb: web::Data<Handlebars<'_>>,
offers: web::Data<JobOffers>,
config: web::Data<ServerConfig>,
) -> actix_web::Result<HttpResponse, ConfirmationError> {
) -> actix_web::Result<HttpResponse, ConfirmationResponseError> {
let id = &path.0;
let req_token = &path.1;
@ -154,9 +154,9 @@ pub(crate) async fn reject_joboffer_post(
.insert_header((http::header::CONTENT_TYPE, HTML_CONTENT.clone()))
.body(body))
} else {
Err(ConfirmationError::InvalidRequest)
Err(ConfirmationResponseError::InvalidRequest)
}
} else {
Err(ConfirmationError::InvalidRequest)
Err(ConfirmationResponseError::InvalidRequest)
}
}

View file

@ -17,14 +17,14 @@ use url::Url;
use crate::auth::User;
use crate::error::{LoginRequired, MultipartFieldError, PresentationError};
use crate::job_offers::error::{EmailError, SaveError};
use crate::job_offers::error::{EmailError, SaveResponseError};
use crate::job_offers::{
Attachment, ConfirmationStatus, JobOffer, JobOfferStatus, JobOffers, Link, MutBorrowedJobOffer,
ReviewStatus,
};
use crate::route::job_offer::confirmation::JOBOFFER_CONFIRM_ROUTE;
use crate::route::job_offer::create::multipart_form::{multi_field, multi_file, once_field};
use crate::route::job_offer::error::SubmissionError;
use crate::route::job_offer::error::SubmissionResponseError;
use crate::route::HTML_CONTENT;
use crate::server_config::{EmailConfig, ServerConfig};
use crate::{template, SubmissionLimiter};
@ -62,7 +62,7 @@ pub(crate) async fn create_joboffer_post(
limiter: web::Data<SubmissionLimiter>,
hb: web::Data<Handlebars<'_>>,
multipart: Multipart,
) -> Result<HttpResponse, SubmissionError> {
) -> Result<HttpResponse, SubmissionResponseError> {
let user = User::current(&session).ok();
debug!("getting lease for new submission");
@ -86,7 +86,7 @@ pub(crate) async fn create_joboffer_post(
if submission_lease.is_none() && user.is_none() {
debug!("failed to get a lease too many requests");
// the user is not authenticated and has reached the quota for un-authenticated users
return Err(SubmissionError::TooManyRequests);
return Err(SubmissionResponseError::TooManyRequests);
}
debug!("got lease, starting to process submission");
@ -112,7 +112,7 @@ pub(crate) async fn create_joboffer_post(
// submission failed end the lease immediately
lease.end().await
}
return Err(SubmissionError::MissingLinkOrAttachment);
return Err(SubmissionResponseError::MissingLinkOrAttachment);
}
debug!("validation successful, saving submitted job offer");
@ -179,7 +179,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
offers: &'data JobOffers,
config: &'config ServerConfig,
user: Option<&User>,
) -> Result<MutBorrowedJobOffer<'data, 'static, 'config>, SaveError> {
) -> Result<MutBorrowedJobOffer<'data, 'static, 'config>, SaveResponseError> {
let now_date = crate::util::now();
let token: String =
@ -193,7 +193,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
{
// a reviewer-only option is set, but user is not logged-in as a reviewer
// maybe a session just expired or some one is messing with the form
return Err(SaveError::LoginRequired(LoginRequired::new()));
return Err(SaveResponseError::Login(LoginRequired::new()));
}
let submission_datetime = user

View file

@ -9,11 +9,11 @@ use log::warn;
use thiserror::Error;
use crate::error::{default_error_response, LoginRequired, MultipartFieldError, PresentationError};
use crate::job_offers::error::{DeleteError, SaveError};
use crate::job_offers::error::{DeleteError, SaveError, SaveResponseError};
use crate::job_offers::JobofferLoadError;
#[derive(Debug, Error)]
pub(crate) enum ConfirmationError {
pub(crate) enum ConfirmationResponseError {
/// The referenced job offer does not exists,
/// or does not await confirmation,
/// or the token was invalid
@ -33,33 +33,37 @@ pub(crate) enum ConfirmationError {
Presentation(#[from] PresentationError),
}
impl ResponseError for ConfirmationError {
impl ResponseError for ConfirmationResponseError {
fn status_code(&self) -> StatusCode {
match self {
ConfirmationError::RenderError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ConfirmationError::SuccessRenderError(_) => StatusCode::CREATED,
ConfirmationError::InvalidRequest => StatusCode::BAD_REQUEST,
ConfirmationError::Save(inner) => inner.status_code(),
ConfirmationError::Delete(inner) => inner.status_code(),
ConfirmationError::Url(inner) => inner.status_code(),
ConfirmationError::Presentation(inner) => inner.status_code(),
ConfirmationResponseError::RenderError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ConfirmationResponseError::SuccessRenderError(_) => StatusCode::CREATED,
ConfirmationResponseError::InvalidRequest => StatusCode::BAD_REQUEST,
ConfirmationResponseError::Save(inner) => inner.as_status_code(),
ConfirmationResponseError::Delete(DeleteError::IO(_)) => {
StatusCode::INTERNAL_SERVER_ERROR
}
ConfirmationResponseError::Url(inner) => inner.status_code(),
ConfirmationResponseError::Presentation(inner) => inner.status_code(),
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let status_code = self.status_code();
match self {
ConfirmationError::SuccessRenderError(inner) => {
ConfirmationResponseError::SuccessRenderError(inner) => {
warn!("Failed to render successful submission response {}", inner);
HttpResponse::build(status_code).body("The submission was successful, but an error occurred while generating this response.")
}
ConfirmationError::InvalidRequest => {
ConfirmationResponseError::InvalidRequest => {
HttpResponse::build(status_code).body("Invalid Request")
} // TODO more detail
ConfirmationError::Save(inner) => inner.error_response(),
ConfirmationError::Delete(inner) => inner.error_response(),
ConfirmationError::Url(inner) => inner.error_response(),
error @ (ConfirmationError::RenderError(_) | ConfirmationError::Presentation(_)) => {
ConfirmationResponseError::Save(_) | ConfirmationResponseError::Delete(_) => {
default_error_response(self, status_code)
}
ConfirmationResponseError::Url(inner) => inner.error_response(),
error @ (ConfirmationResponseError::RenderError(_)
| ConfirmationResponseError::Presentation(_)) => {
default_error_response(error, status_code)
}
}
@ -67,30 +71,30 @@ impl ResponseError for ConfirmationError {
}
#[derive(Debug, Error)]
pub(crate) enum DeletionError {
pub(crate) enum DeletionResponseError {
#[error("Could not delete Job Offer: {0}")]
Delete(#[from] DeleteError),
#[error("Login required to perform job Offer deletion: {0}")]
Login(#[from] LoginRequired),
#[error("{0}")]
Presentation(#[from] PresentationError),
}
impl ResponseError for DeletionError {
impl ResponseError for DeletionResponseError {
fn status_code(&self) -> StatusCode {
match self {
DeletionError::Delete(inner) => inner.status_code(),
DeletionError::Login(inner) => inner.status_code(),
DeletionResponseError::Delete(DeleteError::IO(_))
| DeletionResponseError::Presentation(_) => StatusCode::INTERNAL_SERVER_ERROR,
DeletionResponseError::Login(_) => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
match self {
DeletionError::Delete(inner) => inner.error_response(),
DeletionError::Login(inner) => inner.error_response(),
}
default_error_response(self, self.status_code())
}
}
#[derive(Debug, Error)]
pub(crate) enum StateChangeError {
pub(crate) enum StateChangeResponseError {
#[error("Could not save changes to Job Offer: {0}")]
Save(#[from] SaveError),
#[error("Login required to perform job Offer deletion: {0}")]
@ -99,104 +103,120 @@ pub(crate) enum StateChangeError {
Presentation(#[from] PresentationError),
}
impl From<RenderError> for StateChangeError {
impl From<RenderError> for StateChangeResponseError {
fn from(render: RenderError) -> Self {
Self::Presentation(PresentationError::Render(render))
}
}
impl From<UrlGenerationError> for StateChangeError {
impl From<UrlGenerationError> for StateChangeResponseError {
fn from(url_gen: UrlGenerationError) -> Self {
Self::Presentation(PresentationError::Url(url_gen))
}
}
impl ResponseError for StateChangeError {
impl ResponseError for StateChangeResponseError {
fn status_code(&self) -> StatusCode {
match self {
StateChangeError::Save(inner) => inner.status_code(),
StateChangeError::Login(inner) => inner.status_code(),
StateChangeError::Presentation(inner) => inner.status_code(),
StateChangeResponseError::Save(inner) => inner.as_status_code(),
StateChangeResponseError::Login(_inner) => StatusCode::UNAUTHORIZED,
StateChangeResponseError::Presentation(inner) => inner.status_code(),
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
match self {
StateChangeError::Save(inner) => inner.error_response(),
StateChangeError::Login(inner) => inner.error_response(),
StateChangeError::Presentation(inner) => inner.error_response(),
StateChangeResponseError::Login(_) | StateChangeResponseError::Save(_) => {
default_error_response(self, self.status_code())
}
StateChangeResponseError::Presentation(inner) => inner.error_response(),
}
}
}
#[derive(Debug, Error)]
pub(crate) enum SubmissionError {
pub(crate) enum SubmissionResponseError {
#[error("At least one link or attachment is required!")]
MissingLinkOrAttachment,
#[error("An error occurred while processing the form submission: {0}")]
Form(#[from] MultipartFieldError),
#[error("Failed to save the submission: {0}")]
Save(#[from] SaveError),
Save(#[from] SaveResponseError),
#[error("{0}")]
Render(#[from] PresentationError),
#[error("Too many requests!")]
TooManyRequests,
}
impl From<UrlGenerationError> for SubmissionError {
impl From<UrlGenerationError> for SubmissionResponseError {
fn from(url: UrlGenerationError) -> Self {
Self::Render(PresentationError::Url(url))
}
}
impl From<RenderError> for SubmissionError {
impl From<RenderError> for SubmissionResponseError {
fn from(render: RenderError) -> Self {
Self::Render(PresentationError::Render(render))
}
}
impl ResponseError for SubmissionError {
impl ResponseError for SubmissionResponseError {
fn status_code(&self) -> StatusCode {
match self {
SubmissionError::MissingLinkOrAttachment => StatusCode::BAD_REQUEST,
SubmissionError::Form(inner) => inner.status_code(),
SubmissionError::Save(inner) => inner.status_code(),
SubmissionError::Render(inner) => inner.status_code(),
SubmissionError::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
SubmissionResponseError::MissingLinkOrAttachment => StatusCode::BAD_REQUEST,
SubmissionResponseError::Form(MultipartFieldError::Date { .. })
| SubmissionResponseError::Form(MultipartFieldError::ContentTooLarge { .. })
| SubmissionResponseError::Form(MultipartFieldError::MultipartError(_))
| SubmissionResponseError::Form(MultipartFieldError::NotAFile { .. })
| SubmissionResponseError::Form(MultipartFieldError::TooManyOccurrences { .. })
| SubmissionResponseError::Form(MultipartFieldError::MissingField { .. })
| SubmissionResponseError::Form(MultipartFieldError::UTF8Error(_))
| SubmissionResponseError::Form(MultipartFieldError::InvalidAddress(_)) => {
StatusCode::BAD_REQUEST
}
SubmissionResponseError::Form(MultipartFieldError::IOError(_))
| SubmissionResponseError::Form(MultipartFieldError::Runtime(_)) => {
StatusCode::INTERNAL_SERVER_ERROR
}
SubmissionResponseError::Save(inner) => inner.status_code(),
SubmissionResponseError::Render(inner) => inner.status_code(),
SubmissionResponseError::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let status_code = self.status_code();
match self {
SubmissionError::MissingLinkOrAttachment => default_error_response(self, status_code),
SubmissionError::Form(inner) => inner.error_response(),
SubmissionError::Save(inner) => inner.error_response(),
SubmissionError::Render(inner) => inner.error_response(),
SubmissionError::TooManyRequests => default_error_response(self, status_code),
SubmissionResponseError::Save(inner) => inner.error_response(),
SubmissionResponseError::Render(inner) => inner.error_response(),
SubmissionResponseError::MissingLinkOrAttachment
| SubmissionResponseError::Form(_)
| SubmissionResponseError::TooManyRequests => default_error_response(self, status_code),
}
}
}
#[derive(Debug, Error)]
pub(crate) enum SyncError {
pub(crate) enum SyncResponseError {
#[error("{0}")]
LoginRequired(#[from] LoginRequired),
Login(#[from] LoginRequired),
#[error("{0}")]
Load(#[from] JobofferLoadError),
#[error("{0}")]
Presentation(#[from] PresentationError),
}
impl ResponseError for SyncError {
impl ResponseError for SyncResponseError {
fn status_code(&self) -> StatusCode {
match self {
SyncError::LoginRequired(_) => StatusCode::FORBIDDEN,
SyncError::Load(_) | SyncError::Presentation(_) => StatusCode::INTERNAL_SERVER_ERROR,
SyncResponseError::Login(_) => StatusCode::UNAUTHORIZED,
SyncResponseError::Load(_) | SyncResponseError::Presentation(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum AttachmentError {
pub(crate) enum AttachmentResponseError {
#[error("Job Offer not found!")]
OfferNotFound,
#[error("Viewing this Job Offers Attachments requires Authentication, as the offer is not yet published: {0}")]
@ -205,24 +225,26 @@ pub(crate) enum AttachmentError {
IO(#[from] std::io::Error),
}
impl ResponseError for AttachmentError {
impl ResponseError for AttachmentResponseError {
fn status_code(&self) -> StatusCode {
match self {
AttachmentError::OfferNotFound => StatusCode::NOT_FOUND,
AttachmentError::IO(error) if error.kind() == ErrorKind::NotFound => {
AttachmentResponseError::OfferNotFound => StatusCode::NOT_FOUND,
AttachmentResponseError::IO(error) if error.kind() == ErrorKind::NotFound => {
StatusCode::NOT_FOUND
}
AttachmentError::LoginRequired(inner) => inner.status_code(),
AttachmentError::IO(_) => StatusCode::INTERNAL_SERVER_ERROR,
AttachmentResponseError::LoginRequired(_) => StatusCode::UNAUTHORIZED,
AttachmentResponseError::IO(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let status_code = self.status_code();
match self {
AttachmentError::OfferNotFound => default_error_response(self, status_code),
AttachmentError::LoginRequired(inner) => inner.error_response(),
AttachmentError::IO(inner) => default_error_response(inner, status_code),
AttachmentResponseError::OfferNotFound => default_error_response(self, status_code),
AttachmentResponseError::LoginRequired(_inner) => {
default_error_response(self, status_code)
}
AttachmentResponseError::IO(inner) => default_error_response(inner, status_code),
}
}
}