[Draft] Implement Early deletion by Submitter #37 #38

Merged
ben merged 15 commits from ben/Jobboerse:main into main 2023-01-28 18:00:00 +01:00
87 changed files with 1316 additions and 763 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/target
/job_offers/**
/packages/jobboerse/job_offers/**
/THIRDPARTY.toml.bak

1076
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,64 +1,46 @@
[workspace]
members = [".", "packages/*"]
[package]
name = "jobboerse"
version = "0.2.4"
[workspace.package]
version = "0.2.5"
edition = "2021"
rust-version = "1.60"
rust-version = "1.64"
license = "MIT OR Apache-2.0"
include = ["APACHE-2.0.LICENSE","MIT.LICENSE"]
repository = "https://www.fs-infmath.uni-kiel.de/git/FS-InfMath/Jobboerse"
homepage = "https://www.fs-infmath.uni-kiel.de/wiki/Hauptseite"
license = "MIT OR Apache-2.0"
readme = "README.md"
categories = ["command-line-utilities", "web-programming::http-server"] # https://crates.io/category_slugs max. 5
keywords = ["jobbörse", "webserver", "application"] # max 5. matching [a-zA-Z][a-zA-Z0-9-_]{,19}
description = "This package provides a webserver for joboffser postings"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
dev_mode = []
[dependencies]
[workspace.dependencies]
actix-files = "0.6.2"
actix-web = "4.1.0"
actix-session = { version = "0.7.1", features = ["cookie-session"] }
actix-multipart = "0.4.0"
actix-web = "4.3.0"
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-multipart = "0.5.0"
better_toml_datetime = { path = "packages/better_toml_datetime" }
cargo-bundle-licenses = { version = "0.5.0", default-features = false }
chrono = { version = "0.4.20", default-features = false, features = ["std","clock"] }
chrono-tz = "0.6.3"
clap = { version = "3.2.16", features = ["derive", "env"] }
futures-util = "0.3.21"
handlebars = { version = "4.3.3", features = ["dir_source"] }
cargo-bundle-licenses = { version = "1.0.1", default-features = false }
chrono = { version = "0.4.23", default-features = false, features = ["std","clock"] }
chrono-tz = "0.8.1"
clap = { version = "4.1.4", features = ["derive", "env"] }
futures-util = "0.3.25"
handlebars = { version = "4.3.6", features = ["dir_source"] }
http = "0.2.8"
lettre = { version = "0.10.1", default-features = false, features = ["sendmail-transport", "tokio1", "builder", "serde"] }
# use rustls a native tls library rather than openssl,
# as depending on c dependencies can get annoying even when vendoring
ldap3 = { version = "0.10.5", default-features = false, features = ["tls-rustls"] }
ldap3 = { version = "0.11.1", default-features = false, features = ["tls-rustls"] }
listenfd = "1.0.0"
log = "0.4.17"
mime_guess = "2.0.4"
multipart_helper = {path = "packages/multipart_helper"}
pretty_env_logger = "0.4.0"
rand = "0.8.5"
serde = { version = "1.0.142", features = ["derive"] } # https://doc.rust-lang.org/cargo/reference/manifest.html
serde_json = "1.0.83"
serde = { version = "1.0.152", features = ["derive"] } # https://doc.rust-lang.org/cargo/reference/manifest.html
serde_json = "1.0.91"
tempfile = "3.3.0"
thiserror = "1.0.32"
toml = "0.5.9"
tokio = "1.20.1"
url = { version = "2.2.2", features = ["serde"] }
[build-dependencies]
cargo-bundle-licenses = { version = "0.5.0", default-features = false }
pretty_env_logger = "0.4.0"
log = "0.4.17"
# manually specify autobin here as otherwise some command freak out if we don't copy the src/ folder
[[bin]]
name = "jobboerse"
path = "src/main.rs"
thiserror = "1.0.38"
toml = "0.7.0"
tokio = "1.24.2"
url = { version = "2.3.1", features = ["serde"] }
[profile.release]
strip = true

View file

@ -7,18 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.4] (2022-08-04)
### Fix
- missing lines around lists and headings triggering lints in changelog
- new clippy lints
### Change
- upgrade/update dependencies
- msrv is now 1.64 as we now take advantage of packages inheriting from the workspace
- move root jobboerse package to packages dir
- startup errors are more descriptive
- the confirmation link now works after confirmation so that submissions can be deleted early by the submitter
- this changes the on-disk format slightly as the token now has a different scope and was moved accordingly
### Removed
- `./scripts/adjust_thirdparty.sh` as it is no longer necessary with cargo-bundle-license version 1.0.0
- docker support
- no longer contains a docker file for dev/prod usage
- dockerfile to test arch pkgbuild is still there
## [0.2.4] (2022-08-04)
### Change
- reviewer notice now includes a link to the added job offer
- upgrade dependencies in general
### Fix
- confirmation dialog for when logged-in as a reviewer
- updated chrono dependency fixing issue FS-InfMath/Jobboerse#5
## [0.2.3] (2022-07-27)
### Change
- upgrade lettre dependency from release candidate version to actual release version
- upgrade dependencies in general
- send a publishing notice to the contact address when a submission is published by a review
@ -27,17 +52,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.2.2] (2022-06-26)
### Add
- faq page
### Change
- better highlight confirm/retract buttons
## [0.2.1] (2022-03-10)
### Add
- reviewer link for manually sending a confirmation email
### Change
- some internal cleanup
- reviewer notice email is now send when offer is not pre-approved
- instead of when confirmation is not skipped
@ -46,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
while reviewer notice failure is not
### 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
@ -54,12 +84,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.2.0] (2022-06-09)
### Add
- ability to edit job offers by reviewers after submission
- ability for reviewers to filter for offer requiring review
- ability to change/remove default footer links and add new ones
- ability to highlight a single job offer
### Change
- improve error handling
- split of two small packages
- a lot of refactoring
@ -75,60 +107,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.6] (2022-05-26)
### Change
- increase some form limits
## [0.1.5] (2022-05-26)
### Change
- some display/style changes for job offer entries
- the confirmation email now uses a handlebar template instead of a compiletime format constant string
### Fix
- /summary should be accessible for anonymous visitors
## [0.1.4] (2022-05-25)
### Fix
- replace incorrectly hardcoded path by config value
## [0.1.3] (2022-05-25)
### Add
- allow reviewers to skip confirmation of new submissions
- allow reviewers to delete all expired entries
### Change
- minimum expiry date for new submissions is now kept up-to-date
- submission form for reviewers now defaults to pre-reviewed and skip-confirmation
### Fix
- index route without base path
- simple login provider should not skip setting the session cookie
## [0.1.2] (2022-05-25)
### Add
- systemd service file
### Change
- improve distribution config
- set storage path in distribution config
- reduce default log level from Trace to Info
### Fix
- incorrect table name in distribution config
## [0.1.1] (2022-05-25)
### Added
- commented default config for distributing
### Change
- strip release build
## [0.1.0] (2022-05-21)
### Added
- Initial Version
- Overview/Landing Page of published Job Offers
- Reviewer Login with LDAP Integration

View file

@ -1,78 +0,0 @@
FROM alpine AS source
WORKDIR /source
COPY build.rs Cargo.lock Cargo.toml deny.toml THIRDPARTY.toml ./
COPY src ./src
COPY static ./static
COPY templates ./templates
FROM rust:alpine AS rust-with-musl-dev
RUN apk add --no-cache musl-dev
FROM rust-with-musl-dev AS check-deny
WORKDIR /deny
# required for building vendored openssl :(
RUN apk add --no-cache perl make
RUN cargo install cargo-deny --locked
COPY Cargo.toml deny.toml ./
RUN cargo deny check
FROM rust-with-musl-dev AS prepare-build
WORKDIR /build
RUN mkdir /jobboerse
# only copy Cargo.{toml,lock} to reduce dependencies of the fetch step
COPY Cargo.toml Cargo.lock ./
RUN cargo fetch
COPY build.rs THIRDPARTY.toml ./
COPY src/ ./src/
COPY static/ ./static/
COPY templates/ ./templates/
COPY scripts/ ./scripts/
FROM prepare-build AS build
ARG BUILD_FLAGS=""
RUN sh ./scripts/adjust_thirdparty.sh
RUN diff THIRDPARTY.toml.bak THIRDPARTY.toml || true
RUN cargo build --verbose --release --frozen $BUILD_FLAGS
FROM alpine AS run
ARG UID=1999
ARG GID=1999
EXPOSE 8080/tcp
VOLUME ["/config"]
VOLUME ["/jobboerse/job_offers"]
RUN addgroup -g $GID -S jobboerse-server
RUN adduser -u $UID -G jobboerse-server -D -Hh /jobboerse jobboerse-server
WORKDIR /jobboerse
COPY --from=build /build/target/release/jobboerse ./
RUN chmod a+x /jobboerse/jobboerse
RUN chmod a+r -R /jobboerse /config
COPY --from=build /build/static/ ./static/
COPY --from=build /build/templates/ ./templates/
USER jobboerse-server
ENTRYPOINT ["/jobboerse/jobboerse"]
CMD ["--config", "/config/config.toml", "--mode", "production"]

33
Dockerfile-pkgbuild Normal file
View file

@ -0,0 +1,33 @@
FROM archlinux:base-devel AS source
ARG UID=1999
ARG GID=1999
EXPOSE 8080/tcp
VOLUME [ "/var/lib/jobboerse/job_offers" ]
RUN echo $UID $GID
RUN groupadd -g $GID dev
RUN useradd -u $UID -g dev -m dev
RUN echo -e "\ndev ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN pacman -Syu --noconfirm
COPY . /src_dir
RUN chown -R dev:dev /src_dir
COPY ./packages/jobboerse/config/login.toml /usr/lib/jobboerse/config/
USER dev
WORKDIR /src_dir/dist/arch/devel
RUN id
RUN makepkg -siC --noconfirm
WORKDIR /usr/lib/jobboerse/
ENTRYPOINT ["/usr/bin/jobboerse"]
CMD ["--config", "/etc/jobboerse/config.toml", "--mode", "production"]

View file

@ -6,7 +6,7 @@ This project contains a web-server application to serve a listing of job offers
Building the binary
-------------------
As the program is writen in rust, a rust toolchain needs to be installed.
As the program is written in rust, a rust toolchain needs to be installed.
Instructions on how to install rustup, the standard toolchain manager for rust, can be found at <https://www.rust-lang.org/tools/install>.
The minimum required rust-toolchain version as of writing is `1.58` see the `rust-version` entry in the `Cargo.toml` file.
@ -45,13 +45,14 @@ Should the config path not exist, then a default config will be used and written
The port 8080 will be used by default, this can be changed with the `--port` flag.
The default log level is `INFO`, logging can be configured via the `RUST_LOG` environment variable as described
in the [`env_logger` documnentation](https://docs.rs/env_logger/0.7.1/env_logger/index.html).
in the [`env_logger` documentation](https://docs.rs/env_logger/0.7.1/env_logger/index.html).
Note: Currently the documentation for version 0.7.1 is relevant, even if it not the newest version.
An update of `pretty_env_logger` should hopefully be available soon to change this.
See [PR 49](https://github.com/seanmonstar/pretty-env-logger/pull/49) in the pretty_env_logger repo, which updates the env_logger dependency.
Config
------
The config file uses the toml format.
The expected fields are defined by the `ProgramConfig` struct in `./src/server_config.rs`.
@ -75,13 +76,12 @@ The `Simple` login provider requires the `file_path` to be set, which points to
a `users` table.
The table keys are then used as usernames and the associated values are expected to be strings containing plaintext passwords.
The `Ldap` login provider takes the follwoing configuration fields.
The `Ldap` login provider takes the following configuration fields.
The `server_address` field specifies a URL under which the ldap server is reached.
The `starttls` options defines whether StartTLS should be used (default is true, though ignored if incompatible with URL).
The `ldap_user_dn` contains a pattern for the user dn for simple bind as well as search `%{username}` is replaced by the ldap-dn-escaped username.
The `dap_user_filter` specifies a filter pattern for an ldap search, `%{username}` is replaced by the ldap-escaped username.
`THIRDPARTY.toml`
-----------------
@ -89,18 +89,14 @@ This file is generated using the `cargo-bundle-licenses` tool and contains licen
It is manually adjusted to include all not auto-detected licenses.
When re-generating this file make sure to not lose still relevant manually inserted licenses and to add newly missing licenses.
Currently, `cargo-bundle-licenses` includes absolute paths for some licenses.
These will differ between machines and usual point into the current user's home directory.
Wrong paths might cause the test, which checks that the `THIRDPARTY.toml` file is up-to-date, to fail.
The `./scripts/adjust_thirdparty.sh` script should fix the paths to match the current environment.
Alternatively, the `THIRDPARTY.toml` may be regenerated as described below.
Regenerating this file usually requires some manual intervention, to fix licenses that were not automatically detected.
To regenerate this file the cargo-bundle-licenses tool needs to be available.
It can be installed via `cargo install cargo-bundle-licenses`.
At least version 0.5.0, as of writing the latest version, is required, for proper handling of complex spdx expressions.
At least version 1.0.0, is required, for normalization of license file path located under `$CARGO_HOME`.
It can then be run with `cargo bundle-licenses --format toml --output THIRDPARTY.toml` to re-generate the file.
The script `./scripts/generate_thirparty.sh` does just that.
The script `./scripts/generate_thirdparty.sh` does just that.
This needs to be done when dependencies change to adjust the corresponding entries.
Make sure to look for entries for which the license text is listed as `NOT FOUND` and insert the appropriate license.
@ -115,14 +111,13 @@ cargo-deny (installed separately): `cargo deny check`
cargo-msrc (installed separately): `cargo msrv --verify`
Cutting a Release
-----------------
* Update the version in the root Cargo.toml according to semver, this will be the version to-be-cut
* Update the changelog to reflect all changes since the last release unter `[Unreleased]`
* It's generally recommended to keep the Changelog upto date by adding changes to the unreleased section in the commit that introduces the change
* In the now up-to-date changelog add a new section heading for the version to-be-cut between `[Unreleasd]` and the first entry of the unreleased section
* In the now up-to-date changelog add a new section heading for the version to-be-cut between `[Unreleased]` and the first entry of the unreleased section
* Add a matching link definition at the bottom of a changelog
* Update the version in dist/arch/PKGBUILD to match the version to-be-cut
* run cargo test to update the version in the Cargo.lock file and check that the tests pass

BIN
THIRDPARTY.toml (Stored with Git LFS)

Binary file not shown.

View file

@ -1,10 +0,0 @@
url_base_path = "jobbörse"
banner = "Hinweis: Die Jobbörse wird aktuall noch evaluiert und befindet sich noch nichts im produktiven Betrieb!"
data_storage_path = "/jobboerse/job_offers"
[login_provider]
type = 'Development'
[email]
from = "jobs@localhost"
subject = "Test"

6
dist/arch/PKGBUILD vendored
View file

@ -33,8 +33,6 @@ prepare()
git lfs pull lfs-remote
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
./scripts/adjust_thirdparty.sh
}
@ -77,14 +75,14 @@ package()
install -dm0755 "${pkgdir}/usr/lib/${_pkgname}/"
cp -r "static/" "templates/" "${pkgdir}/usr/lib/${_pkgname}/"
cp -r "packages/jobboerse/static/" "packages/jobboerse/templates/" "${pkgdir}/usr/lib/${_pkgname}/"
install -Dm0644 -t "${pkgdir}/usr/share/doc/${_pkgname}" \
README.md \
Changelog.md
install -Dm0644 \
"config/dist-config.toml" \
"packages/jobboerse/config/dist-config.toml" \
"${pkgdir}/etc/${_pkgname}/config.toml"
install -Dm0644 \

View file

@ -43,8 +43,6 @@ prepare()
git lfs pull lfs-remote
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
./scripts/adjust_thirdparty.sh
}
@ -88,14 +86,14 @@ package()
install -dm0755 "${pkgdir}/usr/lib/${_pkgname}/"
cp -r "static/" "templates/" "${pkgdir}/usr/lib/${_pkgname}/"
cp -r "packages/jobboerse/static/" "packages/jobboerse/templates/" "${pkgdir}/usr/lib/${_pkgname}/"
install -Dm0644 -t "${pkgdir}/usr/share/doc/${_pkgname}" \
README.md \
Changelog.md
install -Dm0644 \
"config/dist-config.toml" \
"packages/jobboerse/config/dist-config.toml" \
"${pkgdir}/etc/${_pkgname}/config.toml"
install -Dm0644 \

View file

@ -1,13 +1,19 @@
[package]
name = "better_toml_datetime"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
include = ["../../APACHE-2.0.LICENSE","../../MIT.LICENSE"]
description = "Small helper crate for usage in the jobboerse crate containing some helpers for working with toml date, datetime and time structs"
keywords = ["toml", "date", "time"]
categories = []
version.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0.137"
toml = "0.5.9"
thiserror = "1.0.31"
serde = { workspace = true }
toml = { workspace = true }
thiserror = { workspace = true }

View file

@ -3,7 +3,6 @@ use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::str::FromStr;
use thiserror;
use toml::value::DatetimeParseError;
#[derive(Serialize, Deserialize, Clone)]
@ -157,14 +156,14 @@ impl TryFrom<toml::value::Datetime> for Time {
#[derive(Clone, Debug, Hash)]
pub enum Offset {
Z,
Custom { hours: i8, minutes: u8 },
Custom { minutes: i16 },
}
impl From<toml::value::Offset> for Offset {
fn from(toml_offset: toml::value::Offset) -> Self {
match toml_offset {
toml::value::Offset::Z => Self::Z,
toml::value::Offset::Custom { hours, minutes } => Self::Custom { hours, minutes },
toml::value::Offset::Custom { minutes } => Self::Custom { minutes },
}
}
}
@ -173,7 +172,7 @@ impl From<Offset> for toml::value::Offset {
fn from(our_offset: Offset) -> Self {
match our_offset {
Offset::Z => Self::Z,
Offset::Custom { hours, minutes } => Self::Custom { hours, minutes },
Offset::Custom { minutes } => Self::Custom { minutes },
}
}
}

View file

@ -0,0 +1,60 @@
[package]
name = "jobboerse"
categories = ["command-line-utilities", "web-programming::http-server"] # https://crates.io/category_slugs max. 5
keywords = ["jobbörse", "webserver", "application"] # max 5. matching [a-zA-Z][a-zA-Z0-9-_]{,19}
description = "This package provides a webserver for joboffser postings"
include = ["THIRDPARTY.toml", "../APACHE-2.0.LICENSE", "../MIT.LICENSE"]
version.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
dev_mode = []
[dependencies]
actix-files = { workspace = true }
actix-web = { workspace = true }
actix-session = { workspace = true, features = ["cookie-session"] }
actix-multipart = { workspace = true }
better_toml_datetime = { workspace = true }
cargo-bundle-licenses = { workspace = true, default-features = false }
chrono = { workspace = true, default-features = false, features = ["std","clock"] }
chrono-tz = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
futures-util = { workspace = true }
handlebars = { workspace = true, features = ["dir_source"] }
http = { workspace = true }
lettre = { workspace = true, default-features = false, features = ["sendmail-transport", "tokio1", "builder", "serde"] }
# use rustls a native tls library rather than openssl,
# as depending on c dependencies can get annoying even when vendoring
ldap3 = { workspace = true, default-features = false, features = ["tls-rustls"] }
listenfd = { workspace = true }
log = { workspace = true }
mime_guess = { workspace = true }
multipart_helper = { workspace = true }
pretty_env_logger = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] } # https://doc.rust-lang.org/cargo/reference/manifest.html
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true }
url = { workspace = true, features = ["serde"] }
[build-dependencies]
cargo-bundle-licenses = { workspace = true, default-features = false }
pretty_env_logger = { workspace = true }
log = { workspace = true }
# manually specify autobin here as otherwise some command freak out if we don't copy the src/ folder
[[bin]]
name = "jobboerse"
path = "src/main.rs"

BIN
packages/jobboerse/THIRDPARTY.toml (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,50 @@
# the base URL Path under which the jobboerse will be accessible (defaults to the root path)
# url_base_path = "jobbörse"
# the path under which the server will store the job offers (default "./job_offers")
data_storage_path = "/var/lib/jobboerse/job_offers"
# when specefied a banner will be placed at the top of the web-page with the content of this field
# banner = ""
# Configuration of the login provider which shall handle the processing of login information submitter through the login dialouge
[login_provider]
# the type of the configured login provider
# for production builds possible options are: Disabled, Simple or Ldap
type = 'Simple' # deny all login attempts without further configuration options
file_path = '/etc/jobboerse/login.toml'
# type = 'Simple' # lookup username password pairs in a file
# file_path = 'config/login.toml' # toml file containing a `users` table with usernames as the keys and clear-text passwords as values
# type = 'Ldap' # use an ldap server for authentication
# server_address = 'ldap://ldap.example.com' # ldap server URL
# ldap_user_dn = 'uid=%{username},ou=People,dc=example,dc=com' # user DN template , %{username} will be replaced by the provided username
# ldap_user_filter = '(&(objectClass=posixAccount)(uid=%{username}))' # search filter template, %{username} will be replaced by the provided username
# starttls = true # use starttls when applicable for the server_address URL (defaults to true)
# for development builds the value `Development` is also available
# type = 'Development' # accepts all username password combinations without further configuration options USE WITH CARE!!!
# The configuration for sending cofirmation emails
# when not specefied no confirmation emails will be send and submitters won't be able to confirm their submissions
# [email]
# # content of the FROM header for send emails
# from = "jobs@example.com"
# # content of the SUBJECT header for the confirmation emails
# subject = "[Jobbörse] Please, confirm your job-offer submission."
# you can add additional footer links by adding [[footer_links]] entries
# [[footer_links]]
# title = "Example"
# url = "https://example.com"
# the default footer links Impressum, Homepage and Source Repository can be overriten by adding a matching [[footer_links]] entry
# [[footer_links]]
# title = "Homepage"
# url = "https://example.com/home"
# default footer links can also be removed by adding a matching section here without an URL
# [[footer_links]]
# title = "Source Repository"

View file

@ -61,13 +61,25 @@ impl ResponseError for PresentationError {
}
#[derive(Error, Debug)]
#[error("the jobboerse server encountered a fatal error during startup: {0}")]
pub(crate) enum SeverInitializationError {
#[error("the jobboerse server encountered a fatal config error during startup: {0}")]
ConfigError(#[from] ConfigError),
#[error("the jobboerse server encountered a fatal license bundle error during startup: {0}")]
LicenseBundleError(#[from] bundle_licenses_lib::format::FormatError),
#[error(
"the jobboerse server encountered a fatal license joboffer load error during startup: {0}"
)]
JobOfferload(#[from] JobofferLoadError),
#[error("the jobboerse server encountered a fatal template error during startup: {0}")]
TemplateError(#[from] handlebars::TemplateError),
IO(#[from] std::io::Error),
#[error("the jobboerse server encountered a fatal io error during startup, could not bind socket: {0}")]
Bind(std::io::Error),
#[error("the jobboerse server encountered a fatal io error during startup, could not listen on socket: {0}")]
TakeListen(std::io::Error),
#[error("the jobboerse server encountered a fatal io error during startup, could not take listen socket: {0}")]
Listen(std::io::Error),
#[error("the jobboerse server encountered a fatal io error during startup, could not start the server: {0}")]
Run(std::io::Error),
}
pub(crate) fn default_error_response(

View file

@ -108,8 +108,17 @@ pub(crate) struct Link {
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
#[serde(tag = "type")]
#[serde(tag = "type", deny_unknown_fields)]
pub enum ConfirmationStatus {
AwaitingConfirmation {
// work around for https://github.com/serde-rs/serde/issues/2294
},
Confirmed,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
#[serde(tag = "type")]
pub enum V1ConfirmationStatus {
AwaitingConfirmation { token: String },
Confirmed,
}
@ -126,18 +135,49 @@ pub enum ReviewStatus {
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
pub struct JobOfferStatus {
#[serde(default)]
submitter_token: Option<String>,
review_status: ReviewStatus,
confirmation_status: ConfirmationStatus,
}
impl From<V1JobOfferStatus> for JobOfferStatus {
fn from(
V1JobOfferStatus {
review_status,
confirmation_status,
}: V1JobOfferStatus,
) -> Self {
let (confirmation_status, token) = match confirmation_status {
V1ConfirmationStatus::AwaitingConfirmation { token } => {
(ConfirmationStatus::AwaitingConfirmation {}, Some(token))
}
V1ConfirmationStatus::Confirmed => (ConfirmationStatus::Confirmed, None),
};
JobOfferStatus {
review_status,
confirmation_status,
submitter_token: token,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
pub struct V1JobOfferStatus {
review_status: ReviewStatus,
confirmation_status: V1ConfirmationStatus,
}
impl JobOfferStatus {
pub(crate) fn new(
review_status: ReviewStatus,
confirmation_status: ConfirmationStatus,
submitter_token: String,
) -> Self {
Self {
review_status,
confirmation_status,
submitter_token: Some(submitter_token),
}
}
@ -155,15 +195,12 @@ impl JobOfferStatus {
}
}
pub fn check_confirmation_token(&self, presented_token: &str) -> bool {
match &self.confirmation_status {
ConfirmationStatus::AwaitingConfirmation { token } => token == presented_token,
ConfirmationStatus::Confirmed => false,
}
pub fn check_submitter_token(&self, presented_token: &str) -> bool {
self.submitter_token.as_deref() == Some(presented_token)
}
pub fn mark_as_confirmed(&mut self, presented_token: &str) -> Result<(), ()> {
if self.check_confirmation_token(presented_token) {
if self.check_submitter_token(presented_token) {
self.confirmation_status = ConfirmationStatus::Confirmed;
Ok(())
} else {
@ -177,6 +214,7 @@ impl JobOfferStatus {
JobOfferStatus {
review_status: ReviewStatus::Reviewed,
confirmation_status: ConfirmationStatus::Confirmed,
submitter_token: _
}
)
}
@ -216,6 +254,43 @@ pub struct JobOffer<AttachmentLocation> {
pub(crate) links: Vec<Link>,
}
impl From<V1JobOffer> for JobOffer<PathBuf> {
fn from(old: V1JobOffer) -> Self {
JobOffer {
title: old.title,
offering_party: old.offering_party,
public_contact_info: old.public_contact_info,
date_of_submission: old.date_of_submission,
date_of_expiry: old.date_of_expiry,
permanent: old.permanent,
contact_info: old.contact_info,
status: old.status.into(),
attachments: old.attachments,
links: old.links,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Hash)]
pub struct V1JobOffer {
pub(crate) title: String,
pub(crate) offering_party: String,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub(crate) public_contact_info: bool,
pub(crate) date_of_submission: better_toml_datetime::Datetime,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub(crate) date_of_expiry: Option<Date>,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub(crate) permanent: bool,
// complex fields need to come after simple ones for derived serialization to work with toml format!
pub(crate) contact_info: Address,
pub(crate) status: V1JobOfferStatus,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub(crate) attachments: Vec<Attachment<PathBuf>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub(crate) links: Vec<Link>,
}
impl Deref for JobOffer<PathBuf> {
type Target = JobOfferStatus;
@ -336,13 +411,12 @@ impl JobOffer<PathBuf> {
.map(|date| toml_date_to_chrono_date(&date.0))
.unwrap_or_else(|| {
toml_datetime_to_chrono_datetime(&self.date_of_submission)
.date()
.add(chrono::Duration::days(6 * 30))
});
use chrono::Offset as _;
let now = crate::util::now();
let now_date = now.with_timezone(&now.offset().fix()).date();
let now_date = now.with_timezone(&now.offset().fix());
now_date > expires_after
}
@ -364,7 +438,7 @@ impl JobOffer<PathBuf> {
Ok(Attachment {
title: attachment.title.clone(),
file_name: attachment.file_name.clone(),
attachment_location: attachment.generate_link(id, req, None)?.into(),
attachment_location: attachment.generate_link(id, req, None)?,
})
})
.collect::<Result<Vec<_>, UrlGenerationError>>()?;
@ -441,7 +515,7 @@ impl JobOffer<PathBuf> {
.map(|attachment| {
let location = match (is_preview && !self.is_published(), confirmation_token) {
(false, _) => attachment.generate_link(id, req, None)?.to_string(),
(true, Some(token)) if self.check_confirmation_token(token) => {
(true, Some(token)) if self.check_submitter_token(token) => {
attachment.generate_link(id, req, Some(token))?.to_string()
}
(true, _) => preview_location.to_string(),
@ -497,6 +571,50 @@ impl JobOffer<PathBuf> {
url.set_fragment(Some(&format!("joboffer-{}", id)));
Ok(url)
}
#[cfg(test)]
pub(crate) fn new_for_test() -> Self {
use std::str::FromStr;
Self {
title: "Test".to_string(),
offering_party: "Test".to_string(),
public_contact_info: false,
date_of_submission: better_toml_datetime::Datetime {
date: Some(better_toml_datetime::Date(toml::value::Date {
year: 2022,
month: 10,
day: 31,
})),
time: Some(better_toml_datetime::Time(toml::value::Time {
hour: 12,
minute: 11,
second: 10,
nanosecond: 9,
})),
offset: Some(better_toml_datetime::Offset::Z),
},
date_of_expiry: Some(Date(toml::value::Date {
year: 2022,
month: 10,
day: 31,
})),
permanent: false,
contact_info: Address::from_str("test@example.com")
.expect("the hardcoded value should be fine"),
status: JobOfferStatus {
submitter_token: Some("Token".to_string()),
review_status: ReviewStatus::AwaitingReview,
confirmation_status: ConfirmationStatus::AwaitingConfirmation {},
},
attachments: vec![Attachment {
title: "Attachment".to_string(),
file_name: "filename.txt".to_string(),
attachment_location: PathBuf::from("some_path.attachment"),
}],
links: vec![],
}
}
}
pub struct JobOffers {
@ -507,8 +625,8 @@ type JobOfferId = str;
#[derive(Debug, thiserror::Error)]
pub(crate) enum JobofferLoadError {
#[error("{0}")]
IO(#[from] std::io::Error),
#[error("Failed to load Job Offer file at {1}: {0}")]
IO(std::io::Error, PathBuf),
#[error("Failed to deserialize Job Offer {id}: {err}")]
TomlDeserializationError { id: String, err: toml::de::Error },
}
@ -536,12 +654,19 @@ impl JobOffers {
let storage = &config.config.data_storage_path;
info!("Loading Job Offers from {}", storage.display());
let mut dir = tokio::fs::read_dir(storage).await?;
let mut dir = tokio::fs::read_dir(storage)
.await
.map_err(|err| JobofferLoadError::IO(err, storage.clone()))?;
while let Some(entry) = dir.next_entry().await.transpose() {
let entry = entry?;
if entry.file_type().await?.is_dir() {
let entry = entry.map_err(|err| JobofferLoadError::IO(err, storage.clone()))?;
let path = entry.path();
if entry
.file_type()
.await
.map_err(|err| JobofferLoadError::IO(err, path.clone()))?
.is_dir()
{
let index = path.join("index.toml");
if index.is_file() {
if let Some(key) = path
@ -550,13 +675,10 @@ impl JobOffers {
.to_str()
.map(str::to_string)
{
let file_content = tokio::fs::read_to_string(index).await?;
let job = toml::de::from_str(&file_content).map_err(|err| {
JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
}
})?;
let file_content = tokio::fs::read_to_string(&index)
.await
.map_err(|err| JobofferLoadError::IO(err, index))?;
let job = Self::load_job_offer(file_content, &key).await?;
job_offers.insert(key, job);
}
}
@ -568,6 +690,32 @@ impl JobOffers {
Ok(())
}
async fn load_job_offer(
file_content: String,
key: &String,
) -> Result<JobOffer<PathBuf>, JobofferLoadError> {
let job = toml::de::from_str(&file_content)
.map_err(|err| JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
})
.or_else(|_prev_err| {
let v1: V1JobOffer = toml::de::from_str(&file_content).map_err(|err| {
JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
}
})?;
// migration will only be persisted on the next change to the joboffer
info!(
"In-Memory migration applied while loading joboffer id: {}",
key
);
Ok(v1.into())
})?;
Ok(job)
}
pub(crate) async fn create_new_offer<'data, 'config, Tz>(
&'data self,
submission_time: chrono::DateTime<Tz>,
@ -578,7 +726,7 @@ impl JobOffers {
Tz: TimeZone,
Tz::Offset: std::fmt::Display,
{
let submission_date = submission_time.date().format("%Y-%m-%d").to_string();
let submission_date = submission_time.format("%F").to_string();
let seconds_since_midnight = submission_time.num_seconds_from_midnight();
let guard = self.data.write().await;
@ -644,27 +792,29 @@ impl JobOffers {
id: &JobOfferId,
only_expired: bool,
config: &ServerConfig,
) -> Result<(), DeleteError> {
) -> Result<Option<JobOffer<PathBuf>>, DeleteError> {
use std::collections::btree_map::Entry;
// we want to keep the guard around for the whole operation not
// jut for the remove step, otherwise another thread might add a new offer under the same id,
// while we have yet to delete the old one causing us to delete the new offer
let mut guard = self.data.write().await;
match guard.entry(id.to_owned()) {
Ok(match guard.entry(id.to_owned()) {
Entry::Vacant(_) => {
warn!("Tried to delete non-existent job-offer: {}", id)
warn!("Tried to delete non-existent job-offer: {}", id);
None
}
Entry::Occupied(entry) => {
if !only_expired || entry.get().is_expired() {
entry.remove();
let result = entry.remove();
let folder_path = JobOffer::<PathBuf>::folder_path(id, config);
tokio::fs::remove_dir_all(folder_path).await?;
Some(result)
} else {
None
}
}
}
Ok(())
})
}
}
@ -734,7 +884,7 @@ impl<'id> MutBorrowedJobOffer<'_, 'id, '_> {
}
pub(crate) fn mark_as_confirmed(&mut self, token: &str) -> Result<(), ()> {
let () = self.data.status.mark_as_confirmed(token)?;
self.data.status.mark_as_confirmed(token)?;
self.dirty = Dirty::Yes;
Ok(())
}
@ -782,3 +932,34 @@ impl Deref for MutBorrowedJobOffer<'_, '_, '_> {
self.data.deref()
}
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use super::{JobOffer, JobOfferStatus};
#[test]
fn joboffers_can_be_serialized() -> Result<(), toml::ser::Error> {
toml::ser::to_string_pretty(&JobOffer::<PathBuf>::new_for_test()).map(|_| ())
}
#[test]
fn v1_should_not_deserialize_as_current() {
let old = super::V1JobOfferStatus {
review_status: super::ReviewStatus::AwaitingReview,
confirmation_status: super::V1ConfirmationStatus::AwaitingConfirmation {
token: "Test".to_string(),
},
};
let serialized = toml::ser::to_string_pretty(&old)
.expect("testing serialization is not the goal of this test");
println!("{}", serialized);
toml::de::from_str::<JobOfferStatus>(&serialized).expect_err(
"V1 should not parse as current, this will prevent migration from applying",
);
}
}

View file

@ -80,11 +80,13 @@ impl JobOfferActions {
.url_for(JOBOFFER_EDIT_ROUTE, &[id])
.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,
let confirmation_url = match &offer.status {
JobOfferStatus {
confirmation_status: ConfirmationStatus::AwaitingConfirmation {},
submitter_token: Some(token),
review_status: _,
} => Some(req.url_for(JOBOFFER_CONFIRM_ROUTE, [id, token])?),
_ => None,
};
let highlight_url = offer.highlight_link(id, req)?;

View file

@ -14,6 +14,7 @@ use actix_web::cookie::Key;
use actix_web::http::StatusCode;
use actix_web::middleware::{ErrorHandlers, NormalizePath, TrailingSlash};
use actix_web::{get, web, App, HttpServer};
use error::SeverInitializationError;
use handlebars::Handlebars;
use listenfd::ListenFd;
use log::{error, LevelFilter, SetLoggerError};
@ -150,15 +151,21 @@ async fn run() -> Result<(), error::SeverInitializationError> {
app
});
if let Some(l) = listen_fd.take_tcp_listener(0)? {
server.listen(l)?
if let Some(l) = listen_fd
.take_tcp_listener(0)
.map_err(SeverInitializationError::TakeListen)?
{
server.listen(l).map_err(SeverInitializationError::Listen)?
} else {
let ipv6 = SocketAddr::from((Ipv6Addr::UNSPECIFIED, port));
let ipv4 = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port));
server.bind([ipv6, ipv4].as_slice())?
server
.bind([ipv6, ipv4].as_slice())
.map_err(SeverInitializationError::Bind)?
}
.run()
.await?;
.await
.map_err(SeverInitializationError::Run)?;
Ok(())
}

View file

@ -125,7 +125,7 @@ fn base<'a>(
config: &'a ServerConfig,
title: &'a str,
) -> Result<BaseData<'a>, UrlGenerationError> {
let index_css = req.url_for_static(INDEX_CSS_ROUTE)?.into();
let index_css = req.url_for_static(INDEX_CSS_ROUTE)?;
let static_routes = StaticRoutes::new(req)?;
@ -175,7 +175,7 @@ fn base<'a>(
banner: config.config.banner.clone(),
operation_mode: config.args.mode.clone(),
dev_build: dev_available,
date: crate::util::now().date().format("%F").to_string(),
date: crate::util::now().format("%F").to_string(),
};
Ok(data)

View file

@ -22,7 +22,6 @@ use lettre::address::AddressError;
use log::{error, warn};
use multipart_helper::MultipartFieldError;
use serde_json::json;
use thiserror::private::DisplayAsDisplay;
use url::Url;
#[derive(Debug, thiserror::Error)]
@ -80,7 +79,10 @@ pub(crate) fn internal_server_error_handler<B>(
} 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::<SubmissionResponseError>() {
error!("Internal Server Error due to SubmissionError: {}", err)
error!(
"Internal Server Error due to SubmissionResponseError: {}",
err
)
} else if let Some(err) = err.as_error::<EditResponseError>() {
error!("Internal Server Error due to EditResponseError: {}", err)
} else {
@ -274,7 +276,7 @@ pub(crate) fn unauthorized_error_handler<B>(
// for a get request we can just return the user to the originally requested page after login,
// so redirect them to the login page and set the return_to url parameter to the target of the current request
let req_uri = res.request().uri().as_display().to_string();
let req_uri = res.request().uri().to_string();
let login_url = login_url_with_return(res.request(), &req_uri)?;

View file

@ -70,4 +70,4 @@ pub(crate) const ATTACHMENT_FILENAME_EDIT_FIELD: &str = "file_name_edit[]";
pub(crate) const ATTACHMENT_TITLE_EDIT_FIELD: &str = "file_title_edit[]";
pub(crate) const ATTACHMENT_FILE_REPLACE_FIELD: &str = "file_replace[]";
pub(crate) const DELETE_ATTACHMENT_FIELD: &'static str = "delete_attachment[]";
pub(crate) const DELETE_ATTACHMENT_FIELD: &str = "delete_attachment[]";

View file

@ -145,7 +145,7 @@ pub(crate) async fn job_offer_attachment(
&& !query
.token
.as_deref()
.map_or(false, |token| offer.check_confirmation_token(token))
.map_or(false, |token| offer.check_submitter_token(token))
{
let dest = req
.url_for(JOBOFFER_ATTACHMENT_ROUTE, &[id, attachment_name])

View file

@ -23,6 +23,7 @@ struct ConfirmJobOfferViewData {
preview: JobOfferViewData,
actions: ConfirmActions,
is_reviewed: bool,
is_confirmed: bool,
}
pub(crate) const JOBOFFER_CONFIRM_ROUTE: &str = "joboffer_submission_confirm";
@ -40,7 +41,7 @@ pub(crate) async fn confirm_joboffer_get(
let req_token = &path.1;
if let Some(job_offer) = offers.get_offer(id).await {
if !job_offer.check_confirmation_token(req_token) {
if !job_offer.check_submitter_token(req_token) {
Err(ConfirmationResponseError::InvalidRequest)
} else {
let user = User::current(&session).ok();
@ -56,6 +57,7 @@ pub(crate) async fn confirm_joboffer_get(
.to_string(),
},
is_reviewed: !job_offer.status.requires_review(),
is_confirmed: !job_offer.status.requires_confirmation(),
};
let data = json!({
@ -137,13 +139,25 @@ pub(crate) async fn reject_joboffer_post(
let user = User::current(&session).ok();
if let Some(job_offer) = offers.get_offer(id).await {
if job_offer.check_confirmation_token(req_token) {
if job_offer.check_submitter_token(req_token) {
// can't do this after delete as another request might race us between from and delete,
// so we might not get a job offer back from delete, if the other request does the delete first
let was_confirmed = !job_offer.status.requires_confirmation();
// early drop necessary as we can't hold a borrow to the job offer while deleting it
drop(job_offer);
offers.delete_offer(id, false, &config).await?;
let _job_offer = offers.delete_offer(id, false, &config).await?;
let title = if was_confirmed {
"Job Offer Deletion Successful"
} else {
"Job Offer Retraction Successful"
};
let data = json!({
"base": crate::route::base(&req, &config, "Job Offer Retraction Successful")?,
"base": crate::route::base(&req, &config, title)?,
"user": user,
"was_confirmed": was_confirmed,
});
let body = hb

View file

@ -228,9 +228,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
let confirmation_status = if skip_confirmation {
ConfirmationStatus::Confirmed
} else {
ConfirmationStatus::AwaitingConfirmation {
token: token.clone(),
}
ConfirmationStatus::AwaitingConfirmation {}
};
let job_offer = JobOffer {
@ -243,7 +241,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
permanent: is_permanent,
attachments: job_offer_form.attachments,
links: job_offer_form.links,
status: JobOfferStatus::new(review_status, confirmation_status),
status: JobOfferStatus::new(review_status, confirmation_status, token.clone()),
};
let created_offer = offers.create_new_offer(now_date, job_offer, config).await?;
@ -252,7 +250,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
if !is_pre_approved {
match created_offer.highlight_link(req) {
Ok(highlight_url) => {
email::send_reviewer_notice(hb, &email_config, highlight_url).await;
email::send_reviewer_notice(hb, email_config, highlight_url).await;
}
Err(err) => {
error!("Failed to generate highlight link: {}", err)
@ -268,7 +266,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
job_offer_form.contact,
email_config,
&ConfirmationEmailData {
confirmation_link: confirm_url.into(),
confirmation_link: confirm_url,
},
)
.await?;
@ -438,7 +436,7 @@ impl JobOfferSubmitForm {
}
name => {
warn!(
"Unknown field `{}` in multipart form: {}",
"Unknown field `{}` in multipart form: {:?}",
name,
field.content_type()
);

View file

@ -417,7 +417,7 @@ impl JobOfferEditForm {
}
name => {
warn!(
"Unknown field `{}` in multipart form: {}",
"Unknown field `{}` in multipart form: {:?}",
name,
field.content_type()
);

View file

@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use clap::{ArgEnum, StructOpt};
use clap::{Parser, ValueEnum};
use lettre::message::Mailbox;
use log::{info, warn};
use serde::Deserialize;
@ -95,7 +95,7 @@ impl ProgramConfig {
}
}
#[derive(StructOpt, Debug)]
#[derive(Parser, Debug)]
#[structopt(name = "jobboerse", version)]
pub(crate) struct ProgramArguments {
#[structopt(long, env = "JOBBOERSE_CONFIG", default_value_os_t = ProgramArguments::default_config())]
@ -104,7 +104,7 @@ pub(crate) struct ProgramArguments {
#[structopt(short, long, env = "JOBBOERSE_PORT", default_value_t = 8080)]
pub(crate) port: u16,
#[structopt(long, env = "JOBBOERSE_MODE", arg_enum, default_value_t)]
#[structopt(long, env = "JOBBOERSE_MODE", value_enum, default_value_t)]
pub(crate) mode: OperationMode,
}
@ -132,7 +132,7 @@ impl ProgramArguments {
}
}
#[derive(Debug, Clone, ArgEnum, Serialize)]
#[derive(Debug, Clone, ValueEnum, Serialize)]
pub(crate) enum OperationMode {
Production,
#[cfg(feature = "dev_mode")]

View file

@ -1,6 +1,6 @@
use crate::job_offers::{Attachment, Link};
use better_toml_datetime::Offset;
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime, Offset as _, TimeZone, Utc};
use serde::{Deserialize, Deserializer};
use std::str::FromStr;
use tempfile::NamedTempFile;
@ -20,7 +20,6 @@ pub fn chrono_datetime_to_toml_datetime<Tz: chrono::TimeZone>(
let offset_seconds = datetime.offset().fix().local_minus_utc();
let offset_minutes = offset_seconds / 60;
let offset_hours = offset_minutes / 60;
toml::value::Datetime {
date: Some(toml::value::Date {
@ -53,25 +52,22 @@ pub fn chrono_datetime_to_toml_datetime<Tz: chrono::TimeZone>(
nanosecond: datetime.nanosecond(),
}),
offset: Some(toml::value::Offset::Custom {
hours: offset_hours.try_into().expect(
"the hours of the offset should be in the range -12 to 12 and as such fit in an i8",
),
minutes: (offset_minutes % 60).abs().try_into().expect(
"the minutes of the offset should be in the range 0 to 59 and as such fit in an i8",
// especially since we have a % 60 and abs() here
),
minutes: offset_minutes as i16,
}),
}
.into()
}
pub fn toml_date_to_chrono_date(date: &toml::value::Date) -> chrono::Date<FixedOffset> {
let local_date = NaiveDate::from_ymd(date.year.into(), date.month.into(), date.day.into());
pub fn toml_date_to_chrono_date(date: &toml::value::Date) -> chrono::DateTime<FixedOffset> {
let local_date =
NaiveDate::from_ymd_opt(date.year.into(), date.month.into(), date.day.into()).unwrap();
let offset = chrono_tz::Europe::Berlin
.offset_from_local_date(&local_date)
.map(|offset| offset.fix())
.unwrap();
offset.from_local_date(&local_date).unwrap()
offset
.from_local_datetime(&local_date.and_time(NaiveTime::default()))
.unwrap()
}
pub fn toml_datetime_to_chrono_datetime(
@ -81,43 +77,38 @@ pub fn toml_datetime_to_chrono_datetime(
.date
.as_ref()
.expect("datetime should contain a date");
let toml_time = datetime.time.to_owned().unwrap_or(
// fallback to midnight
let toml_time = datetime.time.to_owned().unwrap_or_else(|| {
toml::value::Time {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
}
.into(),
);
.into()
});
let local_datetime = chrono::NaiveDate::from_ymd(
let local_datetime = chrono::NaiveDate::from_ymd_opt(
toml_date.year.into(),
toml_date.month.into(),
toml_date.day.into(),
)
.and_hms_nano(
.unwrap()
.and_hms_nano_opt(
toml_time.hour.into(),
toml_time.minute.into(),
toml_time.second.into(),
toml_time.nanosecond,
);
)
.unwrap();
let offset: FixedOffset = datetime
.offset
.as_ref()
.map(|offset| match offset {
Offset::Z => 0,
Offset::Custom { hours, minutes } => {
let hours: i32 = (*hours).into();
let minutes: i32 = (*minutes).into();
let offset_in_minutes = 60 * hours + hours.signum() * minutes;
offset_in_minutes * 60
}
Offset::Custom { minutes } => *minutes as i32 * 60,
})
.map(FixedOffset::east)
.and_then(|sec_offset| FixedOffset::east_opt(sec_offset))
.unwrap_or_else(|| {
// try to interpret timestamp as local Europe/Berlin time
let tz_result = chrono_tz::Europe::Berlin

View file

@ -8,6 +8,8 @@ Bitte überprüfen Sie die Stellenausschreibung unter dem folgenden Link.
Dort können sie die Stellenausschreibung bestätigen oder zurückziehen.
Nach der Bestätigung kann der Link genutzt werden um die Stellenausschreibung vor Ablauf der Gültigkeit zu löschen.
Freundliche Grüße
Das Team der FS-InfMath Jobbörse

View file

@ -0,0 +1,33 @@
{{#> base}}
<div class="centered column">
<div class="preview">
<h2>Vorschau:</h2>
{{> job_offer/overview-entry}}
</div>
<div class="confirmation-actions">
{{#if job_offer.is_confirmed }}
{{#> confirm-modal}}
{{#*inline "id"}}{{job_offer.id}}{{/inline}}
{{#*inline "kind"}}retract{{/inline}}
{{#*inline "action"}}Löschen{{/inline}}
{{#*inline "formaction"}}{{job_offer.actions.retract_url}}{{/inline}}
{{/confirm-modal}}
{{else}}
{{#> confirm-modal}}
{{#*inline "id"}}{{job_offer.id}}{{/inline}}
{{#*inline "kind"}}confirm{{/inline}}
{{#*inline "action"}}Bestätigen{{#if job_offer.is_reviewed}} und veröffentlichen!{{/if}}{{/inline}}
{{#*inline "formaction"}}{{job_offer.actions.confirm_url}}{{/inline}}
{{/confirm-modal}}
{{#> confirm-modal}}
{{#*inline "id"}}{{job_offer.id}}{{/inline}}
{{#*inline "kind"}}retract{{/inline}}
{{#*inline "action"}}Zurückziehen{{/inline}}
{{#*inline "formaction"}}{{job_offer.actions.retract_url}}{{/inline}}
{{/confirm-modal}}
{{/if}}
</div>
</div>
{{/base}}

View file

@ -1,15 +1,21 @@
[package]
name = "multipart_helper"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
include = ["../../APACHE-2.0.LICENSE", "../../MIT.LICENSE"]
description = "A helper crate for handling multipart forms in the jobbörse create"
keywords = ["multipart/form-data"]
categories = []
version.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-multipart = "0.4.0"
futures-util = "0.3.21"
tempfile = "3.3.0"
thiserror = "1.0.31"
toml = "0.5.9"
actix-multipart = { workspace = true }
futures-util = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }

View file

@ -1,6 +0,0 @@
#!/usr/bin/env bash
# workaround for https://github.com/sstadick/cargo-bundle-licenses/issues/8
CARGO_HOME="${CARGO_HOME:-${HOME}/.cargo}"
sed --in-place=.bak -E "\:file \(.*/\.cargo/:{s:file \(.*/\.cargo/:file \(${CARGO_HOME}/:g;}" ./THIRDPARTY.toml

View file

@ -1,8 +0,0 @@
#!/usr/bin/env bash
SCRIPT_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
set -e
source ${SCRIPT_DIR}/build_dev_image.sh
source ${SCRIPT_DIR}/run_dev_container.sh

View file

@ -1,7 +0,0 @@
#!/usr/bin/env bash
USER=${SUDO_USER:-$(whoami)}
echo "Generating for ${USER}"
docker build --rm --build-arg UID="$(id -u "${USER}")" --build-arg GID="$(id -g "${USER}")" --build-arg BUILD_FLAGS="--features=dev_mode" --tag jobboerse:dev .

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker build --rm --build-arg UID=$(id -u ${SUDO_USER:-$(whoami)}) --build-arg GID=$(id -g ${SUDO_USER:-$(whoami)}) --tag jobboerse:prod .

16
scripts/dev.sh Normal file → Executable file
View file

@ -2,5 +2,17 @@
#RUST_BACKTRACE=1
systemfd --no-pid -s http::8080 -- \
cargo watch -i ./config/dev-config.toml -x 'run --package jobboerse --bin jobboerse --release --features=dev_mode -- --config=./config/dev-config.toml --mode=development'
DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
CFG="${DIR}/../packages/jobboerse/config/dev-config.toml"
RUN_DIR="${DIR}/../packages/jobboerse"
pushd "${RUN_DIR}"
systemfd \
--no-pid \
-s http::8080 \
-- cargo watch \
-i ./packages/jobboerse/config/dev-config.toml \
-x "run --package jobboerse --bin jobboerse --release --features=dev_mode -- --config=\"${CFG}\" --mode=development"
popd

13
scripts/generate_thirdparty.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
OUT="${DIR}/../packages/jobboerse/THIRDPARTY.toml"
RUN_DIR="${DIR}/../packages/jobboerse"
pushd "${RUN_DIR}"
cargo bundle-licenses \
--format toml \
--output "${OUT}"
popd

View file

@ -1,5 +0,0 @@
#!/bin/bash
cargo bundle-licenses --format toml --output THIRDPARTY.toml

15
scripts/prod.sh Normal file → Executable file
View file

@ -1,3 +1,16 @@
#!/bin/bash
cargo run --package jobboerse --bin jobboerse --release --config=./config/prod-config.toml
DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
CFG="${DIR}/../packages/jobboerse/config/prod-config.toml"
RUN_DIR="${DIR}/../packages/jobboerse"
pushd "${RUN_DIR}"
cargo run \
--package jobboerse \
--bin jobboerse \
--release \
-- \
--config "${CFG}"
popd

View file

@ -1,8 +0,0 @@
docker run \
--rm \
-p 8080:8080 \
-v "$(pwd)"/job_offers:/jobboerse/job_offers \
-v "$(pwd)"/config:/config \
jobboerse:dev \
--config=/config/dev-docker-config.toml \
--mode=development

28
scripts/test-pkgbuild.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
USER=${SUDO_USER:-$(whoami)}
echo "Generating for ${USER}"
set -e
echo "Building docker image"
# needs root as otherwise we can't run it with root later (images will be in the users images but runing from root images)
# see run_dev_conatiner.sh for why that needs root
sudo docker build \
--rm \
--build-arg UID="$(id -u "${USER}")" \
--build-arg GID="$(id -g "${USER}")" \
-t jobboerse:pkgbuild \
-f ./Dockerfile-pkgbuild \
.
echo "Starting empheral container"
# root is necessary for the volums to be mounted correctly
# otherwise they will be owned by root instead of the correct user
sudo docker run \
--rm \
-p 8080:8080 \
-v "$(pwd)"/job_offers:/var/lib/jobboerse/job_offers \
-v "$(pwd)"/packages/jobboerse/config/dist-test-config.toml:/etc/jobboerse/config.toml \
-v "$(pwd)"/packages/jobboerse/config/login.toml:/etc/jobboerse/login.toml \
jobboerse:pkgbuild

View file

@ -1,24 +0,0 @@
{{#> base}}
<div class="centered column">
<div class="preview">
<h2>Vorschau:</h2>
{{> job_offer/overview-entry}}
</div>
<div class="confirmation-actions">
{{#> confirm-modal}}
{{#*inline "id"}}{{job_offer.id}}{{/inline}}
{{#*inline "kind"}}confirm{{/inline}}
{{#*inline "action"}}Bestätigen{{#if job_offer.reviewed}} und veröffentlichen!{{/if}}{{/inline}}
{{#*inline "formaction"}}{{job_offer.actions.confirm_url}}{{/inline}}
{{/confirm-modal}}
{{#> confirm-modal}}
{{#*inline "id"}}{{job_offer.id}}{{/inline}}
{{#*inline "kind"}}retract{{/inline}}
{{#*inline "action"}}Zurückziehen{{/inline}}
{{#*inline "formaction"}}{{job_offer.actions.retract_url}}{{/inline}}
{{/confirm-modal}}
</div>
</div>
{{/base}}