attempt at fixing confirmation emails by setting email headers, link for manual confirmation email and cut next release #29
16 changed files with 165 additions and 115 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -1211,7 +1211,7 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobboerse"
|
name = "jobboerse"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
|
|
@ -2523,6 +2523,7 @@ dependencies = [
|
||||||
"idna",
|
"idna",
|
||||||
"matches",
|
"matches",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ members = [".", "packages/*"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "jobboerse"
|
name = "jobboerse"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.58"
|
rust-version = "1.58"
|
||||||
repository = "https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse"
|
repository = "https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse"
|
||||||
|
|
@ -48,7 +48,7 @@ tempfile = "3.3.0"
|
||||||
thiserror = "1.0.31"
|
thiserror = "1.0.31"
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
tokio = "1.18.2"
|
tokio = "1.18.2"
|
||||||
url = "2.2.2"
|
url = {version = "2.2.2", features = ["serde"]}
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cargo-bundle-licenses = { version = "0.5.0", default-features = false }
|
cargo-bundle-licenses = { version = "0.5.0", default-features = false }
|
||||||
|
|
|
||||||
15
Changelog.md
15
Changelog.md
|
|
@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.1] (2022-03-10)
|
||||||
|
|
||||||
|
### Add
|
||||||
|
- reviewer link for manually sending a confirmation email
|
||||||
|
|
||||||
|
### Change
|
||||||
|
- some internal cleanup
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
- confirmation emails now set the UserAgent and Auto-Submitted headers
|
||||||
|
- the error pages using the generic_error_handler now set the Content-Type header
|
||||||
|
- prevent error on missing edit action
|
||||||
|
- occurred for reviewers submitting job offers as the actions are not set for the preview after submission
|
||||||
|
|
||||||
## [0.2.0] (2022-06-09)
|
## [0.2.0] (2022-06-09)
|
||||||
|
|
||||||
### Add
|
### Add
|
||||||
|
|
@ -92,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Overview Page of Dependency licenses
|
- Overview Page of Dependency licenses
|
||||||
|
|
||||||
[Unreleased]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/main
|
[Unreleased]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/main
|
||||||
|
[0.2.1]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.2.1
|
||||||
[0.2.0]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.2.0
|
[0.2.0]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.2.0
|
||||||
[0.1.6]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.1.6
|
[0.1.6]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.1.6
|
||||||
[0.1.5]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.1.5
|
[0.1.5]: https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse/src/version-0.1.5
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ banner = "Hinweis: Die Jobbörse wird aktuall noch evaluiert und befindet sich n
|
||||||
type = 'Development'
|
type = 'Development'
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
from = "jobs@localhost"
|
from = "Test-Jobbörse <jobs@localhost>"
|
||||||
subject = "Test"
|
subject = "Test"
|
||||||
|
|
||||||
[[footer_links]]
|
[[footer_links]]
|
||||||
|
|
|
||||||
2
dist/arch/PKGBUILD
vendored
2
dist/arch/PKGBUILD
vendored
|
|
@ -9,7 +9,7 @@ _reponame=Jobboerse
|
||||||
_pkgname="${_reponame,,}"
|
_pkgname="${_reponame,,}"
|
||||||
_features=()
|
_features=()
|
||||||
pkgname="${_reponame,,}"
|
pkgname="${_reponame,,}"
|
||||||
pkgver=0.2.0
|
pkgver=0.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="FS-InfMath Job-Offer Page"
|
pkgdesc="FS-InfMath Job-Offer Page"
|
||||||
arch=('x86_64') # Other architectures may work
|
arch=('x86_64') # Other architectures may work
|
||||||
|
|
|
||||||
90
src/email.rs
Normal file
90
src/email.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
use crate::job_offers::error::EmailError;
|
||||||
|
use crate::server_config::EmailConfig;
|
||||||
|
use crate::template;
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use lettre::message::header::{Header, HeaderName, HeaderValue, UserAgent};
|
||||||
|
use lettre::message::{Mailbox, SinglePart};
|
||||||
|
use lettre::{Address, AsyncTransport};
|
||||||
|
use log::warn;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct AutoGeneratedHeader(String);
|
||||||
|
|
||||||
|
impl Default for AutoGeneratedHeader {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self("auto-generated".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for AutoGeneratedHeader {
|
||||||
|
fn name() -> HeaderName {
|
||||||
|
HeaderName::new_from_ascii_str("Auto-Submitted")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(s: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
Ok(Self(s.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&self) -> HeaderValue {
|
||||||
|
HeaderValue::new(Self::name(), self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent() -> UserAgent {
|
||||||
|
String::from("fs-infmath-jobboerse via sendmail").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub(crate) struct EmailData {
|
||||||
|
pub(crate) confirmation_link: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn send_confirmation_email(
|
||||||
|
hb: &Handlebars<'_>,
|
||||||
|
contact_address: Address,
|
||||||
|
email_config: &EmailConfig,
|
||||||
|
email_data: &EmailData,
|
||||||
|
) -> actix_web::Result<(), EmailError> {
|
||||||
|
// receiver of the confirmation e-mail
|
||||||
|
let to_mailbox = Mailbox::new(None, contact_address);
|
||||||
|
|
||||||
|
let email_body = hb.render(template::EMAIL_PLAIN, &email_data)?;
|
||||||
|
|
||||||
|
let message = lettre::Message::builder()
|
||||||
|
.from(email_config.from.to_owned())
|
||||||
|
.to(to_mailbox)
|
||||||
|
.subject(&email_config.subject)
|
||||||
|
.header(user_agent())
|
||||||
|
.header(AutoGeneratedHeader::default())
|
||||||
|
.singlepart(SinglePart::plain(email_body))?;
|
||||||
|
|
||||||
|
lettre::AsyncSendmailTransport::new().send(message).await?;
|
||||||
|
|
||||||
|
// successfully send a confirmation e-mail now send a notice to our-self
|
||||||
|
|
||||||
|
let message = lettre::Message::builder()
|
||||||
|
.from(email_config.from.to_owned())
|
||||||
|
.to(email_config.from.to_owned())
|
||||||
|
.subject(&email_config.subject)
|
||||||
|
.header(user_agent())
|
||||||
|
.header(AutoGeneratedHeader::default())
|
||||||
|
.singlepart(SinglePart::plain(
|
||||||
|
"Automatischer Hinweis: Eine neue Stellenausschreibung wurde zur Jobbörse eingereicht!"
|
||||||
|
.to_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Ok(msg) => {
|
||||||
|
dbg!(&msg);
|
||||||
|
if let Err(err) = lettre::AsyncSendmailTransport::new().send(msg).await {
|
||||||
|
warn!("Failed to send remainder {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to construct reminder {}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -455,7 +455,7 @@ impl JobOffer<PathBuf> {
|
||||||
.collect::<Result<_, PresentationError>>()?;
|
.collect::<Result<_, PresentationError>>()?;
|
||||||
|
|
||||||
let actions = if !is_preview && is_authenticated {
|
let actions = if !is_preview && is_authenticated {
|
||||||
Some(JobOfferActions::new(req, id)?)
|
Some(JobOfferActions::new(req, id, self)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
use crate::job_offers::{Attachment, JobOfferId, JobOfferStatus, Link};
|
use crate::job_offers::{
|
||||||
use crate::route::{
|
Attachment, ConfirmationStatus, JobOffer, JobOfferId, JobOfferStatus, Link,
|
||||||
JOBOFFER_DELETION_ROUTE, JOBOFFER_EDIT_ROUTE, JOBOFFER_PUBLISH_ROUTE, JOBOFFER_UNPUBLISH_ROUTE,
|
};
|
||||||
|
use crate::route::{
|
||||||
|
JOBOFFER_CONFIRM_ROUTE, JOBOFFER_DELETION_ROUTE, JOBOFFER_EDIT_ROUTE, JOBOFFER_PUBLISH_ROUTE,
|
||||||
|
JOBOFFER_UNPUBLISH_ROUTE,
|
||||||
};
|
};
|
||||||
use crate::util::SerializableUrl;
|
|
||||||
use actix_web::error::UrlGenerationError;
|
use actix_web::error::UrlGenerationError;
|
||||||
use actix_web::HttpRequest;
|
use actix_web::HttpRequest;
|
||||||
use chrono::FixedOffset;
|
use chrono::FixedOffset;
|
||||||
use lettre::Address;
|
use lettre::Address;
|
||||||
|
use std::path::PathBuf;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
|
|
@ -20,7 +23,7 @@ pub(crate) struct JobOfferEditData {
|
||||||
pub(crate) expiry_date: Option<String>,
|
pub(crate) expiry_date: Option<String>,
|
||||||
pub(crate) submission_date: String,
|
pub(crate) submission_date: String,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
pub(crate) attachments: Vec<Attachment<SerializableUrl>>,
|
pub(crate) attachments: Vec<Attachment<Url>>,
|
||||||
pub(crate) links: Vec<Option<Link>>,
|
pub(crate) links: Vec<Option<Link>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,20 +50,18 @@ pub(crate) struct JobOfferViewData {
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct JobOfferActions {
|
pub struct JobOfferActions {
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
publish_url: Url,
|
publish_url: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
unpublish_url: Url,
|
unpublish_url: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
delete_url: Url,
|
delete_url: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
edit_url: Url,
|
edit_url: Url,
|
||||||
|
confirmation_link: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JobOfferActions {
|
impl JobOfferActions {
|
||||||
pub(super) fn new(
|
pub(super) fn new(
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
id: &JobOfferId,
|
id: &JobOfferId,
|
||||||
|
offer: &JobOffer<PathBuf>,
|
||||||
) -> actix_web::Result<Self, UrlGenerationError> {
|
) -> actix_web::Result<Self, UrlGenerationError> {
|
||||||
let publish_url = req
|
let publish_url = req
|
||||||
.url_for(JOBOFFER_PUBLISH_ROUTE, &[id])
|
.url_for(JOBOFFER_PUBLISH_ROUTE, &[id])
|
||||||
|
|
@ -78,11 +79,19 @@ impl JobOfferActions {
|
||||||
.url_for(JOBOFFER_EDIT_ROUTE, &[id])
|
.url_for(JOBOFFER_EDIT_ROUTE, &[id])
|
||||||
.expect("generation of delete route urls should succeed");
|
.expect("generation of delete route urls should succeed");
|
||||||
|
|
||||||
|
let confirmation_url = match &offer.status.confirmation_status {
|
||||||
|
ConfirmationStatus::AwaitingConfirmation { token } => {
|
||||||
|
Some(req.url_for(JOBOFFER_CONFIRM_ROUTE, [id, token])?)
|
||||||
|
}
|
||||||
|
ConfirmationStatus::Confirmed => None,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
publish_url,
|
publish_url,
|
||||||
unpublish_url,
|
unpublish_url,
|
||||||
delete_url,
|
delete_url,
|
||||||
edit_url,
|
edit_url,
|
||||||
|
confirmation_link: confirmation_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use job_offers::lease::SubmissionLimiter;
|
||||||
use route::error_handler;
|
use route::error_handler;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod email;
|
||||||
mod error;
|
mod error;
|
||||||
mod job_offers;
|
mod job_offers;
|
||||||
mod route;
|
mod route;
|
||||||
|
|
|
||||||
19
src/route.rs
19
src/route.rs
|
|
@ -17,6 +17,7 @@ mod license;
|
||||||
|
|
||||||
pub(crate) use auth::{LOGIN_ROUTE, LOGOUT_ROUTE};
|
pub(crate) use auth::{LOGIN_ROUTE, LOGOUT_ROUTE};
|
||||||
pub(crate) use job_offer::{
|
pub(crate) use job_offer::{
|
||||||
|
confirmation::JOBOFFER_CONFIRM_ROUTE,
|
||||||
create::JOBOFFER_CREATION_ROUTE,
|
create::JOBOFFER_CREATION_ROUTE,
|
||||||
delete::{JOBOFFER_BULK_DELETE_ROUTE, JOBOFFER_DELETE_EXPIRED_ROUTE, JOBOFFER_DELETION_ROUTE},
|
delete::{JOBOFFER_BULK_DELETE_ROUTE, JOBOFFER_DELETE_EXPIRED_ROUTE, JOBOFFER_DELETION_ROUTE},
|
||||||
edit::JOBOFFER_EDIT_ROUTE,
|
edit::JOBOFFER_EDIT_ROUTE,
|
||||||
|
|
@ -29,7 +30,6 @@ pub(crate) use license::{LICENSES_ROUTE, LICENSE_BUNDLE};
|
||||||
use crate::error::PresentationError;
|
use crate::error::PresentationError;
|
||||||
use crate::server_config::OperationMode;
|
use crate::server_config::OperationMode;
|
||||||
use crate::server_config::ServerConfig;
|
use crate::server_config::ServerConfig;
|
||||||
use crate::util::SerializableUrl;
|
|
||||||
|
|
||||||
static HTML_CONTENT: HeaderValue = HeaderValue::from_static("text/html");
|
static HTML_CONTENT: HeaderValue = HeaderValue::from_static("text/html");
|
||||||
static JSON_CONTENT: HeaderValue = HeaderValue::from_static("application/json");
|
static JSON_CONTENT: HeaderValue = HeaderValue::from_static("application/json");
|
||||||
|
|
@ -60,7 +60,7 @@ struct BaseData<'a> {
|
||||||
title: Cow<'a, str>,
|
title: Cow<'a, str>,
|
||||||
short_lang: Cow<'a, str>,
|
short_lang: Cow<'a, str>,
|
||||||
links: Vec<Link<'a>>,
|
links: Vec<Link<'a>>,
|
||||||
styles: Vec<SerializableUrl>,
|
styles: Vec<Url>,
|
||||||
routes: StaticRoutes,
|
routes: StaticRoutes,
|
||||||
banner: Option<String>,
|
banner: Option<String>,
|
||||||
operation_mode: OperationMode,
|
operation_mode: OperationMode,
|
||||||
|
|
@ -70,25 +70,15 @@ struct BaseData<'a> {
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct StaticRoutes {
|
struct StaticRoutes {
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
licenses: Url,
|
licenses: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
login: Url,
|
login: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
logout: Url,
|
logout: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
sync: Url,
|
sync: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
index: Url,
|
index: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
joboffer_overview: Url,
|
joboffer_overview: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
joboffer_create: Url,
|
joboffer_create: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
joboffer_summary: Url,
|
joboffer_summary: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
joboffers_delete_expired: Url,
|
joboffers_delete_expired: Url,
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
joboffers_bulk_delete: Url,
|
joboffers_bulk_delete: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +145,10 @@ fn base<'a>(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| {
|
.map(|elem| {
|
||||||
default_links.retain(|&(title, _)| title != elem.title);
|
default_links.retain(|&(title, _)| title != elem.title);
|
||||||
(elem.title.as_str(), elem.url.as_deref())
|
(
|
||||||
|
elem.title.as_str(),
|
||||||
|
elem.url.as_ref().map(|elem| elem.as_str()),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::auth::User;
|
use crate::auth::User;
|
||||||
use crate::error::PresentationError;
|
use crate::error::PresentationError;
|
||||||
use crate::job_offers::error::SaveResponseError;
|
use crate::job_offers::error::SaveResponseError;
|
||||||
use crate::route::LOGIN_ROUTE;
|
use crate::route::{HTML_CONTENT, LOGIN_ROUTE};
|
||||||
use crate::{route, ServerConfig};
|
use crate::{route, ServerConfig};
|
||||||
|
|
||||||
use crate::route::job_offer::edit::EditResponseError;
|
use crate::route::job_offer::edit::EditResponseError;
|
||||||
|
|
@ -16,7 +16,7 @@ use actix_web::middleware::ErrorHandlerResponse;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{HttpRequest, HttpResponse, ResponseError};
|
use actix_web::{HttpRequest, HttpResponse, ResponseError};
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use http::header::LOCATION;
|
use http::header::{CONTENT_TYPE, LOCATION};
|
||||||
use http::Method;
|
use http::Method;
|
||||||
use lettre::address::AddressError;
|
use lettre::address::AddressError;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
|
@ -54,9 +54,14 @@ pub(crate) fn generic_server_error_handler<B>(
|
||||||
.render(template, &data)
|
.render(template, &data)
|
||||||
.map_err(PresentationError::Render)?;
|
.map_err(PresentationError::Render)?;
|
||||||
|
|
||||||
let response = HttpResponse::with_body(res.status(), body)
|
let mut response = HttpResponse::with_body(res.status(), body)
|
||||||
.map_into_boxed_body()
|
.map_into_boxed_body()
|
||||||
.map_into_right_body();
|
.map_into_right_body();
|
||||||
|
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(CONTENT_TYPE, HTML_CONTENT.clone());
|
||||||
|
|
||||||
let response = res.into_response(response);
|
let response = res.into_response(response);
|
||||||
|
|
||||||
Ok(ErrorHandlerResponse::Response(response))
|
Ok(ErrorHandlerResponse::Response(response))
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use handlebars::Handlebars;
|
||||||
use http::header::CONTENT_TYPE;
|
use http::header::CONTENT_TYPE;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
pub(crate) mod confirmation;
|
pub(crate) mod confirmation;
|
||||||
pub(crate) mod create;
|
pub(crate) mod create;
|
||||||
|
|
@ -24,7 +25,6 @@ use crate::route::job_offer::error::SyncResponseError;
|
||||||
use crate::route::{HTML_CONTENT, JSON_CONTENT};
|
use crate::route::{HTML_CONTENT, JSON_CONTENT};
|
||||||
use crate::server_config::ServerConfig;
|
use crate::server_config::ServerConfig;
|
||||||
use crate::template;
|
use crate::template;
|
||||||
use crate::util::SerializableUrl;
|
|
||||||
|
|
||||||
pub fn configure(service: &mut ServiceConfig) {
|
pub fn configure(service: &mut ServiceConfig) {
|
||||||
service
|
service
|
||||||
|
|
@ -171,13 +171,13 @@ pub(crate) async fn summary(
|
||||||
struct SummaryData {
|
struct SummaryData {
|
||||||
version: &'static str,
|
version: &'static str,
|
||||||
entries: Vec<JobOfferViewData>,
|
entries: Vec<JobOfferViewData>,
|
||||||
overview: SerializableUrl,
|
overview: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = json!(SummaryData {
|
let data = json!(SummaryData {
|
||||||
version: "1",
|
version: "1",
|
||||||
entries: previews,
|
entries: previews,
|
||||||
overview: SerializableUrl(req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?)
|
overview: req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,17 @@ use actix_session::Session;
|
||||||
use actix_web::{get, post, web, HttpRequest, HttpResponse, Result};
|
use actix_web::{get, post, web, HttpRequest, HttpResponse, Result};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use lettre::message::{Mailbox, SinglePart};
|
use lettre::Address;
|
||||||
use lettre::{Address, AsyncTransport};
|
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use rand::distributions::DistString;
|
use rand::distributions::DistString;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::auth::User;
|
use crate::auth::User;
|
||||||
|
use crate::email::EmailData;
|
||||||
use crate::error::{LoginRequired, PresentationError};
|
use crate::error::{LoginRequired, PresentationError};
|
||||||
use crate::job_offers::error::{EmailError, SaveResponseError};
|
use crate::job_offers::error::SaveResponseError;
|
||||||
use crate::job_offers::lease::SubmissionLimiter;
|
use crate::job_offers::lease::SubmissionLimiter;
|
||||||
use crate::job_offers::{
|
use crate::job_offers::{
|
||||||
Attachment, ConfirmationStatus, JobOffer, JobOfferStatus, JobOffers, Link, MutBorrowedJobOffer,
|
Attachment, ConfirmationStatus, JobOffer, JobOfferStatus, JobOffers, Link, MutBorrowedJobOffer,
|
||||||
|
|
@ -28,9 +27,9 @@ use crate::route::form_constants::{self, UploadLimits};
|
||||||
use crate::route::job_offer::confirmation::JOBOFFER_CONFIRM_ROUTE;
|
use crate::route::job_offer::confirmation::JOBOFFER_CONFIRM_ROUTE;
|
||||||
use crate::route::job_offer::error::{FormProcessingError, SubmissionResponseError};
|
use crate::route::job_offer::error::{FormProcessingError, SubmissionResponseError};
|
||||||
use crate::route::HTML_CONTENT;
|
use crate::route::HTML_CONTENT;
|
||||||
use crate::server_config::{EmailConfig, ServerConfig};
|
use crate::server_config::ServerConfig;
|
||||||
use crate::template;
|
|
||||||
use crate::util::{parse_date, parse_datetime, process_links, process_new_attachments};
|
use crate::util::{parse_date, parse_datetime, process_links, process_new_attachments};
|
||||||
|
use crate::{email, template};
|
||||||
use multipart_helper::{multi_field, multi_file, once_field};
|
use multipart_helper::{multi_field, multi_file, once_field};
|
||||||
|
|
||||||
pub(crate) const JOBOFFER_CREATION_ROUTE: &str = "create_offer";
|
pub(crate) const JOBOFFER_CREATION_ROUTE: &str = "create_offer";
|
||||||
|
|
@ -251,12 +250,12 @@ pub(crate) async fn create_job_offer<'data, 'config>(
|
||||||
if let Some(email_config) = &config.config.email {
|
if let Some(email_config) = &config.config.email {
|
||||||
let confirm_url = req.url_for(JOBOFFER_CONFIRM_ROUTE, &[created_offer.id(), &token])?;
|
let confirm_url = req.url_for(JOBOFFER_CONFIRM_ROUTE, &[created_offer.id(), &token])?;
|
||||||
|
|
||||||
send_confirmation_email(
|
email::send_confirmation_email(
|
||||||
hb,
|
hb,
|
||||||
job_offer_form.contact,
|
job_offer_form.contact,
|
||||||
email_config,
|
email_config,
|
||||||
&EmailData {
|
&EmailData {
|
||||||
confirmation_link: confirm_url,
|
confirmation_link: confirm_url.into(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -266,52 +265,6 @@ pub(crate) async fn create_job_offer<'data, 'config>(
|
||||||
Ok(created_offer)
|
Ok(created_offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct EmailData {
|
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")]
|
|
||||||
confirmation_link: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_confirmation_email(
|
|
||||||
hb: &Handlebars<'_>,
|
|
||||||
contact_address: Address,
|
|
||||||
email_config: &EmailConfig,
|
|
||||||
email_data: &EmailData,
|
|
||||||
) -> Result<(), EmailError> {
|
|
||||||
let to_mailbox = Mailbox::new(None, contact_address);
|
|
||||||
|
|
||||||
let email_body = hb.render(template::EMAIL_PLAIN, &email_data)?;
|
|
||||||
|
|
||||||
let message = lettre::Message::builder()
|
|
||||||
.from(email_config.from.to_owned())
|
|
||||||
.to(to_mailbox)
|
|
||||||
.subject(&email_config.subject)
|
|
||||||
.singlepart(SinglePart::plain(email_body))?;
|
|
||||||
lettre::AsyncSendmailTransport::new().send(message).await?;
|
|
||||||
|
|
||||||
let message = lettre::Message::builder()
|
|
||||||
.from(email_config.from.to_owned())
|
|
||||||
.to(email_config.from.to_owned())
|
|
||||||
.subject(&email_config.subject)
|
|
||||||
.singlepart(SinglePart::plain(
|
|
||||||
"Automatischer Hinweis: Eine neue Stellenausschreibung wurde zur Jobbörse eingereicht!"
|
|
||||||
.to_owned(),
|
|
||||||
));
|
|
||||||
|
|
||||||
match message {
|
|
||||||
Ok(msg) => {
|
|
||||||
if let Err(err) = lettre::AsyncSendmailTransport::new().send(msg).await {
|
|
||||||
warn!("Failed to send remainder {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to construct reminder {}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JobOfferSubmitForm {
|
impl JobOfferSubmitForm {
|
||||||
/// Convert the Multipart struct representing multipart form-data
|
/// Convert the Multipart struct representing multipart form-data
|
||||||
/// into structured form data
|
/// into structured form data
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use log::{info, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::auth::LoginProviderConfig;
|
use crate::auth::LoginProviderConfig;
|
||||||
use crate::error::ConfigError;
|
use crate::error::ConfigError;
|
||||||
|
|
@ -42,7 +43,7 @@ pub(crate) struct ProgramConfig {
|
||||||
pub(crate) struct Link {
|
pub(crate) struct Link {
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) url: Option<String>,
|
pub(crate) url: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
|
||||||
27
src/util.rs
27
src/util.rs
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::job_offers::{Attachment, Link};
|
use crate::job_offers::{Attachment, Link};
|
||||||
use better_toml_datetime::Offset;
|
use better_toml_datetime::Offset;
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
|
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
// we basically don't do any proper error handling here,
|
// we basically don't do any proper error handling here,
|
||||||
// as we mostly expect the format conversion to be infallible
|
// as we mostly expect the format conversion to be infallible
|
||||||
|
|
@ -138,30 +137,6 @@ where
|
||||||
<Option<String> as Deserialize>::deserialize(des).map(|option| option.is_some())
|
<Option<String> as Deserialize>::deserialize(des).map(|option| option.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn url_as_string<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(url.as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(crate) struct SerializableUrl(
|
|
||||||
#[serde(serialize_with = "crate::util::url_as_string")] pub url::Url,
|
|
||||||
);
|
|
||||||
|
|
||||||
impl From<Url> for SerializableUrl {
|
|
||||||
fn from(url: Url) -> Self {
|
|
||||||
Self(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SerializableUrl> for Url {
|
|
||||||
fn from(SerializableUrl(url): SerializableUrl) -> Self {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_date(
|
pub(crate) fn parse_date(
|
||||||
input: Option<&str>,
|
input: Option<&str>,
|
||||||
) -> Result<Option<better_toml_datetime::Date>, <better_toml_datetime::Date as FromStr>::Err> {
|
) -> Result<Option<better_toml_datetime::Date>, <better_toml_datetime::Date as FromStr>::Err> {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@
|
||||||
<div>ID: {{job_offer.id}}</div>
|
<div>ID: {{job_offer.id}}</div>
|
||||||
<div>Review Status: <span class="{{#unless job_offer.reviewed}}unreviewed{{/unless}}">{{job_offer.status.review_status}}</span></div>
|
<div>Review Status: <span class="{{#unless job_offer.reviewed}}unreviewed{{/unless}}">{{job_offer.status.review_status}}</span></div>
|
||||||
<div>Confirmation Status: <span class="{{#unless job_offer.confirmed}}unconfirmed{{/unless}}">{{job_offer.status.confirmation_status.type}}</span></div>
|
<div>Confirmation Status: <span class="{{#unless job_offer.confirmed}}unconfirmed{{/unless}}">{{job_offer.status.confirmation_status.type}}</span></div>
|
||||||
|
{{log job_offer.actions}}
|
||||||
|
{{#if job_offer.actions.confirmation_link}}
|
||||||
|
<div><a href="mailto:{{job_offer.contact_data}}?body=Confirmation%20Link:%20{{job_offer.actions.confirmation_link}}" >Manual Confirmation Mail</a></div>
|
||||||
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#if job_offer.actions }}
|
{{#if job_offer.actions }}
|
||||||
|
|
@ -100,9 +104,12 @@
|
||||||
{{#*inline "formaction"}}{{job_offer.actions.delete_url}}{{/inline}}
|
{{#*inline "formaction"}}{{job_offer.actions.delete_url}}{{/inline}}
|
||||||
{{/confirm-modal}}
|
{{/confirm-modal}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if job_offer.actions.edit_url }}
|
||||||
|
<a href="{{job_offer.actions.edit_url}}">Bearbeiten</a>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<a href="{{job_offer.actions.edit_url}}">Bearbeiten</a>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue