[Draft] Implement Early deletion by Submitter #37 #38
87 changed files with 1316 additions and 763 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/target
|
||||
/job_offers/**
|
||||
/packages/jobboerse/job_offers/**
|
||||
/THIRDPARTY.toml.bak
|
||||
|
|
|
|||
1076
Cargo.lock
generated
1076
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
62
Cargo.toml
62
Cargo.toml
|
|
@ -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
|
||||
|
|
|
|||
61
Changelog.md
61
Changelog.md
|
|
@ -7,37 +7,66 @@ 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
|
||||
- make the job offer id a link to the highlight link
|
||||
- make the job offer id a link to the highlight link
|
||||
|
||||
## [0.2.2] (2022-06-26)
|
||||
|
||||
### Add
|
||||
|
||||
- faq page
|
||||
|
||||
### Change
|
||||
|
||||
- better highlight confirm/retract buttons
|
||||
|
||||
## [0.2.1] (2022-03-10)
|
||||
|
||||
### Add
|
||||
### 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,81 +84,96 @@ 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
|
||||
- split of two small packages
|
||||
- a lot of refactoring
|
||||
- update/upgrade dependencies
|
||||
- reviewer-only settings when not logged in are now an error rather than being silently ignored
|
||||
- delete expired will now goto a preview instead of deleting directly
|
||||
- change the format of the summary endpoint
|
||||
- the top-level is now an object instead of a list
|
||||
- the top-level is now an object instead of a list
|
||||
- the top-level list is not the entries field of the top-level object
|
||||
- an additional version field is added
|
||||
- also an overview field is added with the url of job offer overview
|
||||
|
||||
## [0.1.6] (2022-05-26)
|
||||
|
||||
### Change
|
||||
### 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
|
||||
### 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
|
||||
### 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
|
||||
|
|
|
|||
78
Dockerfile
78
Dockerfile
|
|
@ -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
33
Dockerfile-pkgbuild
Normal 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"]
|
||||
35
README.md
35
README.md
|
|
@ -6,17 +6,17 @@ 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.
|
||||
A stable toolchain is recommended.
|
||||
It is recommended to use cargo for building as such make sure the `cargo` component of the toolchain is installed.
|
||||
It is recommended to use cargo for building as such make sure the `cargo` component of the toolchain is installed.
|
||||
|
||||
It may be necessary to run `./scripts/adjust_thirdparty.sh` to adjust some absolute paths in the `THIRDPARTY.toml`,
|
||||
though this should only be required to run tests.
|
||||
|
||||
For a release build you can run `cargo build --release` this should build all dependencies and place the final binary in
|
||||
For a release build you can run `cargo build --release` this should build all dependencies and place the final binary in
|
||||
the `./target/release/` folder.
|
||||
|
||||
For a development build you may run `cargo build --features=dev-mode`.
|
||||
|
|
@ -45,20 +45,21 @@ 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`.
|
||||
|
||||
| config field | required | default |
|
||||
|---------------------|----------|----------------|
|
||||
| `url_base_path` | false | empty |
|
||||
| `data_storage_path` | false | `./job_offers` |
|
||||
| `data_storage_path` | false | `./job_offers` |
|
||||
| `banner` | false | no banner |
|
||||
| `login_provider` | true | N/A |
|
||||
| `email` | false | no email |
|
||||
|
|
@ -67,47 +68,42 @@ Note: when email is not configured, no confirmation e-mails will be sent, but co
|
|||
|
||||
For `login_provider` there are four types available: `Ldap`, `Simple`, `Disabled`, and `Development`.
|
||||
The last of which is only available when build with the `dev-mode` feature.
|
||||
Selecting a specific type is done by setting the `type` field to the name of the login provider, this is case-sensitive.
|
||||
Selecting a specific type is done by setting the `type` field to the name of the login provider, this is case-sensitive.
|
||||
|
||||
`Disabled` and `Development` have no further options.
|
||||
|
||||
The `Simple` login provider requires the `file_path` to be set, which points to a toml file containing
|
||||
The `Simple` login provider requires the `file_path` to be set, which points to a toml file containing
|
||||
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`
|
||||
-----------------
|
||||
|
||||
This file is generated using the `cargo-bundle-licenses` tool and contains license information for all third-party dependencies.
|
||||
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.
|
||||
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.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
General Tests: `cargo test`
|
||||
General Tests: `cargo test`
|
||||
|
||||
Check Formatting: `cargo fmt --check`
|
||||
|
||||
|
|
@ -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)
BIN
THIRDPARTY.toml
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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
6
dist/arch/PKGBUILD
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
8
dist/arch/devel/PKGBUILD
vendored
8
dist/arch/devel/PKGBUILD
vendored
|
|
@ -43,8 +43,6 @@ prepare()
|
|||
git lfs pull lfs-remote
|
||||
|
||||
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
|
||||
|
||||
./scripts/adjust_thirdparty.sh
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -88,15 +86,15 @@ 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" \
|
||||
"${pkgdir}/etc/${_pkgname}/config.toml"
|
||||
"packages/jobboerse/config/dist-config.toml" \
|
||||
"${pkgdir}/etc/${_pkgname}/config.toml"
|
||||
|
||||
install -Dm0644 \
|
||||
"jobboerse.service" \
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60
packages/jobboerse/Cargo.toml
Normal file
60
packages/jobboerse/Cargo.toml
Normal 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
BIN
packages/jobboerse/THIRDPARTY.toml
(Stored with Git LFS)
Normal file
Binary file not shown.
50
packages/jobboerse/config/dist-test-config.toml
Normal file
50
packages/jobboerse/config/dist-test-config.toml
Normal 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"
|
||||
|
|
@ -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(
|
||||
|
|
@ -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 path = entry.path();
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)?;
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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)?;
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ fn base<'a>(
|
|||
Some("https://www.fs-infmath.uni-kiel.de/wiki/Fachschaften_Informatik_%26_Mathematik:Impressum".into()),
|
||||
),
|
||||
(
|
||||
"Homepage",
|
||||
"Homepage",
|
||||
crate::PROJECT_HOMEPAGE.map(Into::into)
|
||||
),
|
||||
(
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)?;
|
||||
|
||||
|
|
@ -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[]";
|
||||
|
|
@ -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])
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
);
|
||||
|
|
@ -417,7 +417,7 @@ impl JobOfferEditForm {
|
|||
}
|
||||
name => {
|
||||
warn!(
|
||||
"Unknown field `{}` in multipart form: {}",
|
||||
"Unknown field `{}` in multipart form: {:?}",
|
||||
name,
|
||||
field.content_type()
|
||||
);
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
33
packages/jobboerse/templates/job_offer/submission-confirm.hb
Normal file
33
packages/jobboerse/templates/job_offer/submission-confirm.hb
Normal 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}}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 .
|
||||
|
|
@ -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
16
scripts/dev.sh
Normal file → Executable 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
13
scripts/generate_thirdparty.sh
Executable 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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
|
||||
cargo bundle-licenses --format toml --output THIRDPARTY.toml
|
||||
|
||||
15
scripts/prod.sh
Normal file → Executable file
15
scripts/prod.sh
Normal file → Executable 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
|
||||
|
|
|
|||
|
|
@ -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
28
scripts/test-pkgbuild.sh
Executable 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
|
||||
|
|
@ -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}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue