Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
f25e508bbd | |||
b65b4c7a00 | |||
b2969b988d | |||
9666932915 | |||
0b4248604a | |||
5afaac6197 | |||
95f807b51d | |||
cca925f4eb | |||
90ac5c306d | |||
03964d3542 | |||
2774c6e48a | |||
7f5941ba6a | |||
e591b419bb | |||
784b7cea4e | |||
2b9e6cfefd |
@ -6,8 +6,10 @@ SQLX_OFFLINE=true
|
||||
# 64 byte long openssl rand -base64 64
|
||||
SECRET_KEY="changeInProdOrHandAb11111111111111111111111111111111111111111111"
|
||||
HOSTNAME="localhost"
|
||||
WEBMASTER_EMAIL="admin@example.com"
|
||||
SERVER_ADDRESS="127.0.0.1"
|
||||
SERVER_PORT="8080"
|
||||
APP_ENVIRONMENT="development"
|
||||
|
||||
SMTP_SERVER="localhost"
|
||||
SMTP_PORT="1025"
|
||||
|
49
.sqlx/query-2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95.json
generated
Normal file
49
.sqlx/query-2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95.json
generated
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n availability.id,\n availability.userId,\n availability.startTimestamp,\n availability.endTimestamp,\n availability.comment\n FROM availability\n WHERE availability.userId = $1\n AND (availability.endtimestamp = $2\n OR availability.starttimestamp = $3)\n AND (availability.id <> $4 OR $4 IS NULL);\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "userid",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "starttimestamp",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "endtimestamp",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "comment",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95"
|
||||
}
|
22
.sqlx/query-b4edfcac9404060d487db765b8c18ef8b7440699583e0bede95f4d214e668a87.json
generated
Normal file
22
.sqlx/query-b4edfcac9404060d487db765b8c18ef8b7440699583e0bede95f4d214e668a87.json
generated
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM user_ WHERE email = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b4edfcac9404060d487db765b8c18ef8b7440699583e0bede95f4d214e668a87"
|
||||
}
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -797,7 +797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brass-web"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-http",
|
||||
|
100
README.md
100
README.md
@ -1,69 +1,49 @@
|
||||
# Brass
|
||||
A webservice to plan and organize personnel deployment for [Brandsicherheitswachen](https://de.wikipedia.org/wiki/Brandsicherheitswache) (german; fire watch).
|
||||
|
||||
# Key Technologies
|
||||
- [actix-web](https://actix.rs/)
|
||||
- [sqlx](https://github.com/launchbadge/sqlx)
|
||||
- [askama](https://github.com/askama-rs/askama)
|
||||
- [lettre](https://lettre.rs/)
|
||||
- [htmx](https://htmx.org/)
|
||||
- [hyperscript](https://hyperscript.org/)
|
||||
- [bulma](https://bulma.io/)
|
||||
- great inspiration for project structure and tooling: [gerust.rs](https://gerust.rs)
|
||||
|
||||
# Getting started with developing
|
||||
1. Clone the repository.
|
||||
2. Install and configure Postgresql. Create a new database for brass: `createdb brass`.
|
||||
3. TODO: Configure DB name, DB user & pass, DB connection string, ...
|
||||
4. Install sqlx-cli: `cargo install sqlx-cli`
|
||||
5. Migrate the database: `sqlx database setup`
|
||||
6. Create superuse: `cargo r -- createadmin`
|
||||
|
||||
## Useful stuff
|
||||
- cargo-watch, cargo-add
|
||||
3. Configure database connection string in `.env` config file.
|
||||
4. Install required development tools `cargo install <tool>`
|
||||
- sqlx-cli
|
||||
- mailtutan
|
||||
- cargo-watch
|
||||
- cargo-nextest
|
||||
6. Migrate the development and test database: `cargo db migrate -e development` & `cargo db migrate -e test`
|
||||
7. Create superuser: `cargo r -- createadmin`
|
||||
8. Run and recompile application on file change: `cargo w`
|
||||
9. Run tests via nextest and review possible snapshot changes: `cargo t`
|
||||
|
||||
# Build & Deploy
|
||||
1. Clone the repository.
|
||||
2. Build release `cargo b --release`.
|
||||
3. Copy the artifact `target/release/brass-web` to the desired location. Make it executable `chmod +x brass-web`.
|
||||
4. Create Postgresql database on the target host, configure your mail server.
|
||||
5. Configuration for Brass is done via Environment Variables, see `.env` for a list.
|
||||
6. Migrate the database `[LIST_OF_ENV_VARIABLES] brass-web migrate`.
|
||||
7. Create a superuser `[LIST_OF_ENV_VARIABLES] brass-web createadmin`.
|
||||
8. Create some sort of service file (systemd .service, openbsd rc.conf, ...) to run Brass in the background. Examples can be found in `docs/` directory.
|
||||
|
||||
## Example Deployment OpenBSD
|
||||
```
|
||||
#!/bin/ksh
|
||||
# Contributing & Issues
|
||||
Code lies on my private gitea instance, thus there's no easy way for creating issues or making contributions. If you've got an issue or want to contribute, write me an email and we'll figure it out.
|
||||
|
||||
DATABASE_URL=postgresql://brass:pw@localhost/brass
|
||||
SECRET_KEY=""
|
||||
HOSTNAME="brass.tfld.de"
|
||||
SERVER_ADDRESS="127.0.0.1"
|
||||
SERVER_PORT="8081"
|
||||
SMTP_SERVER="localhost"
|
||||
SMTP_PORT="25"
|
||||
SMTP_TLSTYPE="none"
|
||||
ENVLIST="DATABASE_URL=$DATABASE_URL SECRET_KEY=$SECRET_KEY HOSTNAME=$HOSTNAME SERVER_ADDRESS=$SERVER_ADDRESS SERVER_PORT=$SERVER_PORT SMTP_SERVER=$SMTP_SERVER SMTP_PORT=$SMTP_PORT SMTP_TLSTYPE=$SMTP_TLSTYPE"
|
||||
RUST_LOG="info,actix_server=error"
|
||||
# Project Structure
|
||||
- TODO
|
||||
|
||||
ENVLIST="DATABASE_URL=$DATABASE_URL SECRET_KEY=$SECRET_KEY HOSTNAME=$HOSTNAME SERVER_ADDRESS=$SERVER_ADDRESS SERVER_PORT=$SERVER_PORT SMTP_SERVER=$SMTP_SERVER SMTP_LOGIN=$SMTP_LOGIN SMTP_PASSWORD=$SMTP_PASSWORD SMTP_PORT=$SMTP_PORT SMTP_TLSTYPE=$SMTP_TLSTYPE RUST_LOG=$RUST_LOG"
|
||||
# Further Reading
|
||||
More in depth documentation about design decisions, helpful commands and database schema can be found in `docs/` directory.
|
||||
|
||||
daemon="$ENVLIST /usr/local/bin/brass"
|
||||
daemon_user="www"
|
||||
daemon_logger="daemon.info"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
pexp=".*/usr/local/bin/brass.*"
|
||||
|
||||
rc_bg=YES
|
||||
|
||||
rc_cmd $1
|
||||
```
|
||||
|
||||
|
||||
```ini
|
||||
# Postgres
|
||||
# DATABASE_URL=postgres://postgres@localhost/my_database
|
||||
# SQLite
|
||||
DATABASE_URL=postgresql://brass:password@localhost/brass
|
||||
# 64 byte long
|
||||
SECRET_KEY="secret key"
|
||||
HOSTNAME="brass.tfld.de"
|
||||
ADDRESS="127.0.0.1"
|
||||
PORT="8081"
|
||||
|
||||
SMTP_SERVER="localhost"
|
||||
SMTP_PORT="25"
|
||||
# SMTP_LOGIN=""
|
||||
# SMTP_PASSWORD=""
|
||||
SMTP_TLSTYPE="none"
|
||||
```
|
||||
|
||||
## drop test databases
|
||||
```bash
|
||||
for dbname in $(psql -c "copy (select datname from pg_database where datname like 'brass_test_%') to stdout") ; do
|
||||
echo "$dbname"
|
||||
#dropdb -i "$dbname"
|
||||
done
|
||||
```
|
||||
# Copyright & License
|
||||
Copyright 2025 Max Hohlfeld
|
||||
Brass is licensed under [GNU AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html#license-text).
|
||||
|
Binary file not shown.
8
doc/drop_test_databases.md
Normal file
8
doc/drop_test_databases.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Drop all test databases
|
||||
|
||||
```bash
|
||||
for dbname in $(psql -c "copy (select datname from pg_database where datname like 'brass_test_%') to stdout") ; do
|
||||
echo "$dbname"
|
||||
#dropdb -i "$dbname"
|
||||
done
|
||||
```
|
31
doc/example_deployment_openbsd.md
Normal file
31
doc/example_deployment_openbsd.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Example Deployment OpenBSD
|
||||
- list of env variables may not be up to date
|
||||
|
||||
```sh
|
||||
#!/bin/ksh
|
||||
|
||||
DATABASE_URL=postgresql://brass:pw@localhost/brass
|
||||
SECRET_KEY=""
|
||||
HOSTNAME="brass.tfld.de"
|
||||
SERVER_ADDRESS="127.0.0.1"
|
||||
SERVER_PORT="8081"
|
||||
SMTP_SERVER="localhost"
|
||||
SMTP_PORT="25"
|
||||
SMTP_TLSTYPE="none"
|
||||
ENVLIST="DATABASE_URL=$DATABASE_URL SECRET_KEY=$SECRET_KEY HOSTNAME=$HOSTNAME SERVER_ADDRESS=$SERVER_ADDRESS SERVER_PORT=$SERVER_PORT SMTP_SERVER=$SMTP_SERVER SMTP_PORT=$SMTP_PORT SMTP_TLSTYPE=$SMTP_TLSTYPE"
|
||||
RUST_LOG="info,actix_server=error"
|
||||
|
||||
ENVLIST="DATABASE_URL=$DATABASE_URL SECRET_KEY=$SECRET_KEY HOSTNAME=$HOSTNAME SERVER_ADDRESS=$SERVER_ADDRESS SERVER_PORT=$SERVER_PORT SMTP_SERVER=$SMTP_SERVER SMTP_LOGIN=$SMTP_LOGIN SMTP_PASSWORD=$SMTP_PASSWORD SMTP_PORT=$SMTP_PORT SMTP_TLSTYPE=$SMTP_TLSTYPE RUST_LOG=$RUST_LOG"
|
||||
|
||||
daemon="$ENVLIST /usr/local/bin/brass"
|
||||
daemon_user="www"
|
||||
daemon_logger="daemon.info"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
pexp=".*/usr/local/bin/brass.*"
|
||||
|
||||
rc_bg=YES
|
||||
|
||||
rc_cmd $1
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "brass-web"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0"
|
||||
authors = ["Max Hohlfeld <maxhohlfeld@posteo.de>"]
|
||||
@ -37,6 +37,7 @@ tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing-panic = "0.1.2"
|
||||
rust_xlsxwriter = "0.87.0"
|
||||
regex = "1.11.1"
|
||||
|
||||
[build-dependencies]
|
||||
built = "0.7.4"
|
||||
|
@ -15,30 +15,6 @@ snapshot_kind: text
|
||||
<a class="panel-block is-active">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
Tuchuniform
|
||||
</div>
|
||||
|
||||
<div class="level-item buttons are-small">
|
||||
<button class="button is-success is-light" hx-get="/clothing/edit/1" hx-target="closest a">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="button is-danger is-light" hx-delete="/clothing/1" hx-swap="delete"
|
||||
hx-target="closest a" hx-trigger="confirmed">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#trash-2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="panel-block is-active">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
Schutzkleidung Form 1
|
||||
</div>
|
||||
@ -60,6 +36,30 @@ snapshot_kind: text
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="panel-block is-active">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
Tuchuniform
|
||||
</div>
|
||||
|
||||
<div class="level-item buttons are-small">
|
||||
<button class="button is-success is-light" hx-get="/clothing/edit/1" hx-target="closest a">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="button is-danger is-light" hx-delete="/clothing/1" hx-swap="delete"
|
||||
hx-target="closest a" hx-trigger="confirmed">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#trash-2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="panel-block">
|
||||
<button class="button is-link is-light" hx-get="/clothing/new" hx-swap="beforebegin" hx-target="closest div">
|
||||
<svg class="icon">
|
||||
|
@ -124,7 +124,7 @@ snapshot_kind: text
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="label">Führungsassistent durch FF gestellt?</label>
|
||||
<label class="label">Führungsassistent benötigt?</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
|
@ -3,7 +3,7 @@ use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::filters;
|
||||
use crate::models::{Availability, AvailabilityChangeset, Role, User};
|
||||
use crate::models::{Role, User};
|
||||
use crate::utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute};
|
||||
|
||||
pub mod delete;
|
||||
@ -35,16 +35,3 @@ pub struct AvailabilityForm {
|
||||
pub endtime: NaiveTime,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
fn find_adjacend_availability<'a>(
|
||||
changeset: &AvailabilityChangeset,
|
||||
availability_id_to_be_updated: Option<i32>,
|
||||
existing_availabilities: &'a [Availability],
|
||||
) -> Option<&'a Availability> {
|
||||
let existing_availability = existing_availabilities
|
||||
.iter()
|
||||
.filter(|a| availability_id_to_be_updated.is_none_or(|id| a.id != id))
|
||||
.find(|a| a.start == changeset.time.1 || a.end == changeset.time.0);
|
||||
|
||||
return existing_availability;
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::availability::{find_adjacend_availability, AvailabilityForm},
|
||||
endpoints::availability::AvailabilityForm,
|
||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||
utils::{self, ApplicationError},
|
||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||
};
|
||||
|
||||
#[actix_web::post("/availability/new")]
|
||||
@ -14,32 +13,33 @@ pub async fn post(
|
||||
pool: web::Data<PgPool>,
|
||||
form: web::Form<AvailabilityForm>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
let existing_availabilities =
|
||||
Availability::read_by_user_and_date(pool.get_ref(), user.id, &form.startdate).await?;
|
||||
let context = AvailabilityContext {
|
||||
existing_availabilities: existing_availabilities.clone(),
|
||||
};
|
||||
|
||||
let start = form.startdate.and_time(form.starttime);
|
||||
let end = form.enddate.and_time(form.endtime);
|
||||
|
||||
let context = AvailabilityContext {
|
||||
pool: pool.get_ref(),
|
||||
user_id: user.id,
|
||||
availability_to_get_edited: None,
|
||||
};
|
||||
|
||||
let mut changeset = AvailabilityChangeset {
|
||||
time: (start, end),
|
||||
comment: form.comment.clone(),
|
||||
};
|
||||
|
||||
if let Err(e) = changeset.validate_with(&context) {
|
||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
};
|
||||
|
||||
if let Some(a) = find_adjacend_availability(&changeset, None, &existing_availabilities) {
|
||||
let (changeset_start, changeset_end) = changeset.time;
|
||||
|
||||
if a.end == changeset_start {
|
||||
if let Some(a) =
|
||||
Availability::find_adjacent_by_time_for_user(pool.get_ref(), &start, &end, user.id, None)
|
||||
.await?
|
||||
{
|
||||
if a.end == start {
|
||||
changeset.time.0 = a.start;
|
||||
}
|
||||
|
||||
if a.start == changeset_end {
|
||||
if a.start == end {
|
||||
changeset.time.1 = a.end;
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,10 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{
|
||||
availability::{find_adjacend_availability, AvailabilityForm},
|
||||
IdPath,
|
||||
},
|
||||
endpoints::{availability::AvailabilityForm, IdPath},
|
||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||
utils::{self, ApplicationError},
|
||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||
};
|
||||
|
||||
#[actix_web::post("/availability/edit/{id}")]
|
||||
@ -26,39 +22,38 @@ pub async fn post(
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let existing_availabilities: Vec<Availability> =
|
||||
Availability::read_by_user_and_date(pool.get_ref(), user.id, &availability.start.date())
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| a.id != availability.id)
|
||||
.collect();
|
||||
|
||||
let context = AvailabilityContext {
|
||||
existing_availabilities: existing_availabilities.clone(),
|
||||
};
|
||||
|
||||
let start = form.startdate.and_time(form.starttime);
|
||||
let end = form.enddate.and_time(form.endtime);
|
||||
|
||||
let context = AvailabilityContext {
|
||||
pool: pool.get_ref(),
|
||||
user_id: user.id,
|
||||
availability_to_get_edited: Some(availability.id),
|
||||
};
|
||||
|
||||
let mut changeset = AvailabilityChangeset {
|
||||
time: (start, end),
|
||||
comment: form.comment.clone(),
|
||||
};
|
||||
|
||||
if let Err(e) = changeset.validate_with(&context) {
|
||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
};
|
||||
|
||||
if let Some(a) =
|
||||
find_adjacend_availability(&changeset, Some(availability.id), &existing_availabilities)
|
||||
if let Some(a) = Availability::find_adjacent_by_time_for_user(
|
||||
pool.get_ref(),
|
||||
&start,
|
||||
&end,
|
||||
user.id,
|
||||
Some(availability.id),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let (changeset_start, changeset_end) = changeset.time;
|
||||
|
||||
if a.end == changeset_start {
|
||||
if a.end == start {
|
||||
changeset.time.0 = a.start;
|
||||
}
|
||||
|
||||
if a.start == changeset_end {
|
||||
if a.start == end {
|
||||
changeset.time.1 = a.end;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
use chrono::{Datelike, NaiveDate, Utc};
|
||||
use chrono::{Datelike, Months, NaiveDate, Utc};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
@ -14,6 +14,10 @@ struct EventExportTemplate {
|
||||
user: User,
|
||||
areas: Option<Vec<Area>>,
|
||||
daterange: (NaiveDate, NaiveDate),
|
||||
current_month: (NaiveDate, NaiveDate),
|
||||
current_quarter: (NaiveDate, NaiveDate),
|
||||
next_month: (NaiveDate, NaiveDate),
|
||||
next_quarter: (NaiveDate, NaiveDate),
|
||||
}
|
||||
|
||||
#[actix_web::get("/export/events")]
|
||||
@ -32,17 +36,66 @@ pub async fn get(
|
||||
};
|
||||
|
||||
let today = Utc::now().date_naive();
|
||||
let start = NaiveDate::from_ymd_opt(today.year(), today.month0() + 1, 1).unwrap();
|
||||
let end = NaiveDate::from_ymd_opt(today.year(), today.month0() + 2, 1)
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap();
|
||||
let today_plus_month = today.checked_add_months(Months::new(1)).unwrap();
|
||||
let today_plus_three_month = today.checked_add_months(Months::new(3)).unwrap();
|
||||
|
||||
let template = EventExportTemplate {
|
||||
user: user.into_inner(),
|
||||
areas,
|
||||
daterange: (start, end),
|
||||
daterange: (
|
||||
first_day_of_month(&today).unwrap(),
|
||||
last_day_of_month(&today).unwrap(),
|
||||
),
|
||||
current_month: (
|
||||
first_day_of_month(&today).unwrap(),
|
||||
last_day_of_month(&today).unwrap(),
|
||||
),
|
||||
next_month: (
|
||||
first_day_of_month(&today_plus_month).unwrap(),
|
||||
last_day_of_month(&today_plus_month).unwrap(),
|
||||
),
|
||||
current_quarter: (
|
||||
first_day_of_quarter(&today).unwrap(),
|
||||
last_day_of_quarter(&today).unwrap(),
|
||||
),
|
||||
next_quarter: (
|
||||
first_day_of_quarter(&today_plus_three_month).unwrap(),
|
||||
last_day_of_quarter(&today_plus_three_month).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(template.to_response()?)
|
||||
}
|
||||
|
||||
fn first_day_of_month(date: &NaiveDate) -> Option<NaiveDate> {
|
||||
NaiveDate::from_ymd_opt(date.year(), date.month0() + 1, 1)
|
||||
}
|
||||
|
||||
fn last_day_of_month(date: &NaiveDate) -> Option<NaiveDate> {
|
||||
let month0 = date.month0() + 1;
|
||||
let year = if month0 > 11 {
|
||||
date.year() + 1
|
||||
} else {
|
||||
date.year()
|
||||
};
|
||||
|
||||
let month = (month0 % 12) + 1;
|
||||
|
||||
NaiveDate::from_ymd_opt(year, month, 1)?.pred_opt()
|
||||
}
|
||||
|
||||
fn first_day_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
|
||||
let start_month = (date.quarter() * 3) - 2;
|
||||
NaiveDate::from_ymd_opt(date.year(), start_month, 1)
|
||||
}
|
||||
|
||||
fn last_day_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
|
||||
let quarter = date.quarter();
|
||||
let (year, next_month) = if quarter == 4 {
|
||||
(date.year() + 1, 1)
|
||||
} else {
|
||||
(date.year(), (quarter * 3) + 1)
|
||||
};
|
||||
|
||||
NaiveDate::from_ymd_opt(year, next_month, 1)?.pred_opt()
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ use crate::{
|
||||
struct ExportQuery {
|
||||
start: NaiveDate,
|
||||
end: NaiveDate,
|
||||
area: i32,
|
||||
area: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@ -131,10 +131,12 @@ pub async fn get(
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let area = query.area.unwrap_or(user.area_id);
|
||||
|
||||
let rows_to_export = ExportEventRow::read_all_for_timerange_and_area(
|
||||
pool.get_ref(),
|
||||
(query.start, query.end),
|
||||
query.area,
|
||||
area,
|
||||
)
|
||||
.await?;
|
||||
let entries = read(rows_to_export);
|
||||
@ -154,10 +156,10 @@ pub async fn get(
|
||||
"Fkt",
|
||||
"Reserve",
|
||||
];
|
||||
worksheet.write_row(2, 0, HEADER).unwrap();
|
||||
worksheet.write_row(0, 0, HEADER).unwrap();
|
||||
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
let i = (i + 3) as u32;
|
||||
let i = (i + 1) as u32;
|
||||
|
||||
worksheet.write(i, 0, &entry.date).unwrap();
|
||||
worksheet.write(i, 1, &entry.weekday).unwrap();
|
||||
|
@ -7,6 +7,7 @@ use crate::utils::{ApplicationError, Customization, TemplateResponse};
|
||||
#[template(path = "imprint.html")]
|
||||
struct ImprintTemplate {
|
||||
webmaster_mail: String,
|
||||
hostname: String,
|
||||
}
|
||||
|
||||
#[actix_web::get("/imprint")]
|
||||
@ -15,6 +16,7 @@ pub async fn get_imprint(
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
let template = ImprintTemplate {
|
||||
webmaster_mail: customization.webmaster_email.clone(),
|
||||
hostname: customization.hostname.clone(),
|
||||
};
|
||||
|
||||
Ok(template.to_response()?)
|
||||
|
@ -1,11 +1,10 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{user::NewOrEditUserForm, IdPath},
|
||||
models::{Area, Function, Role, User, UserChangeset},
|
||||
utils::ApplicationError,
|
||||
models::{Function, Role, User, UserChangeset},
|
||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
||||
};
|
||||
|
||||
#[actix_web::post("/users/edit/{id}")]
|
||||
@ -28,14 +27,12 @@ pub async fn post_edit(
|
||||
};
|
||||
|
||||
let role = form.role.try_into()?;
|
||||
if user.role == Role::AreaManager && (user.area_id != user_in_db.area_id || role == Role::Admin) {
|
||||
if user.role == Role::AreaManager && (user.area_id != user_in_db.area_id || role == Role::Admin)
|
||||
{
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let area_id = form.area.unwrap_or(user_in_db.area_id);
|
||||
if Area::read_by_id(pool.get_ref(), area_id).await?.is_none() {
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let mut functions = Vec::with_capacity(3);
|
||||
|
||||
@ -53,14 +50,22 @@ pub async fn post_edit(
|
||||
|
||||
let changeset = UserChangeset {
|
||||
name: form.name.clone(),
|
||||
email: form.email.clone(),
|
||||
email: form.email.to_lowercase(),
|
||||
role,
|
||||
functions,
|
||||
area_id,
|
||||
};
|
||||
|
||||
if let Err(e) = changeset.validate() {
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
if let Some(existing_id) = User::exists(pool.get_ref(), &changeset.email).await? {
|
||||
if existing_id != user_in_db.id {
|
||||
return Ok(HttpResponse::UnprocessableEntity()
|
||||
.body("email: an user already exists with the same email"));
|
||||
}
|
||||
}
|
||||
|
||||
let context = DbContext::new(pool.get_ref());
|
||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
||||
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||
};
|
||||
|
||||
User::update(pool.get_ref(), user_in_db.id, changeset).await?;
|
||||
@ -145,4 +150,67 @@ mod tests {
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
assert_eq!(StatusCode::BAD_REQUEST, response.status());
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
async fn email_gets_cast_to_lowercase(context: &DbTestContext) {
|
||||
User::create(&context.db_pool, Faker.fake()).await.unwrap();
|
||||
Area::create(&context.db_pool, "Süd").await.unwrap();
|
||||
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig::new("/users/edit/1").with_role(Role::Admin);
|
||||
|
||||
let new_name: String = Name().fake();
|
||||
let new_mail: String = String::from("NONLowercaseEMAIL@example.com");
|
||||
|
||||
let form = NewOrEditUserForm {
|
||||
name: new_name.clone(),
|
||||
email: new_mail.clone(),
|
||||
role: Role::AreaManager as u8,
|
||||
is_posten: None,
|
||||
is_wachhabender: None,
|
||||
is_fuehrungsassistent: Some(true),
|
||||
area: Some(2),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
let updated_user = User::read_by_id(&context.db_pool, 1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(new_mail.to_lowercase(), updated_user.email);
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
async fn fails_when_email_already_present(context: &DbTestContext) {
|
||||
User::create(&context.db_pool, Faker.fake()).await.unwrap();
|
||||
User::create(&context.db_pool, Faker.fake()).await.unwrap();
|
||||
Area::create(&context.db_pool, "Süd").await.unwrap();
|
||||
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig::new("/users/edit/1").with_role(Role::Admin);
|
||||
|
||||
let second_user = User::read_by_id(&context.db_pool, 2)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_name: String = Name().fake();
|
||||
let new_mail: String = second_user.email;
|
||||
|
||||
let form = NewOrEditUserForm {
|
||||
name: new_name.clone(),
|
||||
email: new_mail.clone(),
|
||||
role: Role::AreaManager as u8,
|
||||
is_posten: None,
|
||||
is_wachhabender: None,
|
||||
is_fuehrungsassistent: Some(true),
|
||||
area: Some(2),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ async fn post(
|
||||
request: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> impl Responder {
|
||||
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email).await {
|
||||
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await {
|
||||
let salt = user.salt.unwrap();
|
||||
|
||||
let hash = hash_plain_password_with_salt(&form.password, &salt).unwrap();
|
||||
|
@ -1,12 +1,11 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::user::NewOrEditUserForm,
|
||||
mail::Mailer,
|
||||
models::{Function, Registration, Role, User, UserChangeset},
|
||||
utils::ApplicationError,
|
||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
||||
};
|
||||
|
||||
#[actix_web::post("/users/new")]
|
||||
@ -47,14 +46,20 @@ pub async fn post_new(
|
||||
|
||||
let changeset = UserChangeset {
|
||||
name: form.name.clone(),
|
||||
email: form.email.clone(),
|
||||
email: form.email.to_lowercase(),
|
||||
role,
|
||||
functions,
|
||||
area_id,
|
||||
};
|
||||
|
||||
if let Err(e) = changeset.validate() {
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
if let Some(_) = User::exists(pool.get_ref(), &changeset.email).await? {
|
||||
return Ok(HttpResponse::UnprocessableEntity()
|
||||
.body("email: an user already exists with the same email"));
|
||||
}
|
||||
|
||||
let context = DbContext::new(pool.get_ref());
|
||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
||||
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||
};
|
||||
|
||||
let id = User::create(pool.get_ref(), changeset).await?;
|
||||
|
@ -29,7 +29,9 @@ async fn post(
|
||||
&& form.password.is_none()
|
||||
&& form.passwordretyped.is_none()
|
||||
{
|
||||
if let Ok(user) = User::read_for_login(pool.get_ref(), form.email.as_ref().unwrap()).await {
|
||||
if let Ok(user) =
|
||||
User::read_for_login(pool.get_ref(), &form.email.as_ref().unwrap().to_lowercase()).await
|
||||
{
|
||||
let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?;
|
||||
mailer
|
||||
.send_forgot_password_mail(&user, &reset.token)
|
||||
|
@ -24,7 +24,7 @@ ur noch ein Passwort festlegen. Kopiere daf=C3=BCr folgenden Link in deinen=
|
||||
|
||||
https://brasiwa-leipzig.de/register?token=3D123456789
|
||||
|
||||
Bitte beachte, dass der Link nur 24 Stunden g=C3=BCltig ist.
|
||||
Bitte beachte, dass der Link nur 5 Tage lang g=C3=BCltig ist.
|
||||
|
||||
Viele Gr=C3=BC=C3=9Fe
|
||||
--boundary
|
||||
@ -41,7 +41,7 @@ nden Link in deinen Browser:</p>
|
||||
|
||||
<p>https://brasiwa-leipzig.de/register?token=3D123456789</p>
|
||||
|
||||
<p>Bitte beachte, dass der Link <b>nur 24 Stunden g=C3=BCltig</b> ist.</p>
|
||||
<p>Bitte beachte, dass der Link <b>nur 5 Tage lang g=C3=BCltig</b> ist.</p>
|
||||
|
||||
<p>Viele Gr=C3=BC=C3=9Fe</p>
|
||||
--boundary--
|
||||
|
@ -52,6 +52,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let mailer = Mailer::new(&config)?;
|
||||
let customization = Customization {
|
||||
webmaster_email: config.webmaster_email.clone(),
|
||||
hostname: config.hostname.clone()
|
||||
};
|
||||
|
||||
handle_command(args.command, &pool, &mailer).await?;
|
||||
|
@ -341,6 +341,49 @@ impl Availability {
|
||||
Ok(availabilities)
|
||||
}
|
||||
|
||||
pub async fn find_adjacent_by_time_for_user(
|
||||
pool: &PgPool,
|
||||
start: &NaiveDateTime,
|
||||
end: &NaiveDateTime,
|
||||
user: i32,
|
||||
availability_to_ignore: Option<i32>,
|
||||
) -> Result<Option<Availability>> {
|
||||
let records = query!(
|
||||
r##"
|
||||
SELECT
|
||||
availability.id,
|
||||
availability.userId,
|
||||
availability.startTimestamp,
|
||||
availability.endTimestamp,
|
||||
availability.comment
|
||||
FROM availability
|
||||
WHERE availability.userId = $1
|
||||
AND (availability.endtimestamp = $2
|
||||
OR availability.starttimestamp = $3)
|
||||
AND (availability.id <> $4 OR $4 IS NULL);
|
||||
"##,
|
||||
user,
|
||||
start.and_utc(),
|
||||
end.and_utc(),
|
||||
availability_to_ignore
|
||||
)
|
||||
.fetch_all(pool) // possible to find up to two availabilities (upper and lower), for now we only pick one and extend it
|
||||
.await?;
|
||||
|
||||
let adjacent_avaialability = records.first().and_then(|r| {
|
||||
Some(Availability {
|
||||
id: r.id,
|
||||
user_id: r.userid,
|
||||
user: None,
|
||||
start: r.starttimestamp.naive_utc(),
|
||||
end: r.endtimestamp.naive_utc(),
|
||||
comment: r.comment.clone(),
|
||||
})
|
||||
});
|
||||
|
||||
Ok(adjacent_avaialability)
|
||||
}
|
||||
|
||||
pub async fn update(pool: &PgPool, id: i32, changeset: AvailabilityChangeset) -> Result<()> {
|
||||
query!(
|
||||
"UPDATE availability SET startTimestamp = $1, endTimestamp = $2, comment = $3 WHERE id = $4",
|
||||
|
@ -1,38 +1,63 @@
|
||||
use chrono::{Days, NaiveDateTime};
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{END_OF_DAY, START_OF_DAY};
|
||||
use crate::{
|
||||
utils::validation::{
|
||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError,
|
||||
},
|
||||
END_OF_DAY, START_OF_DAY,
|
||||
};
|
||||
|
||||
use super::{start_date_time_lies_before_end_date_time, Availability};
|
||||
use super::Availability;
|
||||
|
||||
#[derive(Validate)]
|
||||
#[garde(allow_unvalidated)]
|
||||
#[garde(context(AvailabilityContext))]
|
||||
pub struct AvailabilityChangeset {
|
||||
#[garde(
|
||||
custom(time_is_not_already_made_available),
|
||||
custom(start_date_time_lies_before_end_date_time)
|
||||
)]
|
||||
pub time: (NaiveDateTime, NaiveDateTime),
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AvailabilityContext {
|
||||
pub existing_availabilities: Vec<Availability>,
|
||||
pub struct AvailabilityContext<'a> {
|
||||
pub pool: &'a PgPool,
|
||||
pub user_id: i32,
|
||||
pub availability_to_get_edited: Option<i32>,
|
||||
}
|
||||
|
||||
impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
|
||||
type Context = AvailabilityContext<'a>;
|
||||
|
||||
async fn validate_with_context(
|
||||
&self,
|
||||
context: &'a Self::Context,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let mut existing_availabilities =
|
||||
Availability::read_by_user_and_date(context.pool, context.user_id, &self.time.0.date())
|
||||
.await?;
|
||||
|
||||
if let Some(existing) = context.availability_to_get_edited {
|
||||
existing_availabilities = existing_availabilities
|
||||
.into_iter()
|
||||
.filter(|a| a.id != existing)
|
||||
.collect();
|
||||
}
|
||||
|
||||
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
|
||||
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn time_is_not_already_made_available(
|
||||
value: &(NaiveDateTime, NaiveDateTime),
|
||||
context: &AvailabilityContext,
|
||||
) -> garde::Result {
|
||||
if context.existing_availabilities.is_empty() {
|
||||
existing_availabilities: &Vec<Availability>,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if existing_availabilities.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let free_slots = find_free_date_time_slots(&context.existing_availabilities);
|
||||
let free_slots = find_free_date_time_slots(existing_availabilities);
|
||||
|
||||
if free_slots.is_empty() {
|
||||
return Err(garde::Error::new(
|
||||
return Err(AsyncValidateError::new(
|
||||
"cant create a availability as every time slot is already filled",
|
||||
));
|
||||
}
|
||||
@ -41,7 +66,7 @@ fn time_is_not_already_made_available(
|
||||
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= value.1 && s.1 >= value.1);
|
||||
|
||||
if !free_block_found_for_start || !free_block_found_for_end {
|
||||
return Err(garde::Error::new(
|
||||
return Err(AsyncValidateError::new(
|
||||
"cant create availability as there exists already a availability with the desired time",
|
||||
));
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ use super::{password_reset::Token, Result};
|
||||
|
||||
impl Registration {
|
||||
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<Registration> {
|
||||
let (token, expires) = generate_token_and_expiration(64, TimeDelta::hours(24));
|
||||
let (token, expires) = generate_token_and_expiration(64, TimeDelta::days(5));
|
||||
|
||||
let inserted = query_as!(
|
||||
Registration,
|
||||
|
@ -147,6 +147,14 @@ impl User {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn exists(pool: &PgPool, email: &str) -> Result<Option<i32>> {
|
||||
let record = sqlx::query!("SELECT id FROM user_ WHERE email = $1;", email)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(record.and_then(|r| Some(r.id)))
|
||||
}
|
||||
|
||||
pub async fn read_all(pool: &PgPool) -> anyhow::Result<Vec<User>> {
|
||||
let records = sqlx::query!(
|
||||
r#"
|
||||
|
@ -1,24 +1,44 @@
|
||||
#[cfg(test)]
|
||||
use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy};
|
||||
|
||||
use garde::Validate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::{Function, Role};
|
||||
use crate::utils::validation::{email_is_valid, AsyncValidate, AsyncValidateError, DbContext};
|
||||
|
||||
#[derive(Debug, Validate)]
|
||||
use super::{Area, Function, Role};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Dummy))]
|
||||
#[garde(allow_unvalidated)]
|
||||
pub struct UserChangeset {
|
||||
#[cfg_attr(test, dummy(faker = "Name()"))]
|
||||
pub name: String,
|
||||
#[garde(email)]
|
||||
#[cfg_attr(test, dummy(faker = "SafeEmail()"))]
|
||||
pub email: String,
|
||||
#[cfg_attr(test, dummy(expr = "Role::Staff"))]
|
||||
pub role: Role,
|
||||
#[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
|
||||
pub functions: Vec<Function>,
|
||||
/// check before: must exist and user can create other user for this area
|
||||
#[cfg_attr(test, dummy(expr = "1"))]
|
||||
pub area_id: i32,
|
||||
}
|
||||
|
||||
impl <'a>AsyncValidate<'a> for UserChangeset {
|
||||
type Context = DbContext<'a>;
|
||||
|
||||
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError> {
|
||||
email_is_valid(&self.email)?;
|
||||
area_exists(context.pool, self.area_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn area_exists(pool: &PgPool, id: i32) -> Result<(), AsyncValidateError> {
|
||||
if Area::read_by_id(pool, id).await?.is_none() {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Angegebener Bereich für Nutzer existiert nicht!",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
#[derive(Clone)]
|
||||
pub struct Customization {
|
||||
pub hostname: String,
|
||||
pub webmaster_email: String,
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod app_customization;
|
||||
mod application_error;
|
||||
pub mod auth;
|
||||
mod date_time_format;
|
||||
@ -5,16 +6,16 @@ pub mod event_planning_template;
|
||||
pub mod manage_commands;
|
||||
pub mod password_change;
|
||||
mod template_response_trait;
|
||||
mod app_customization;
|
||||
pub mod token_generation;
|
||||
pub mod validation;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_helper;
|
||||
|
||||
pub use app_customization::Customization;
|
||||
pub use application_error::ApplicationError;
|
||||
pub use date_time_format::DateTimeFormat;
|
||||
pub use template_response_trait::TemplateResponse;
|
||||
pub use app_customization::Customization;
|
||||
|
||||
use chrono::{NaiveDate, Utc};
|
||||
|
||||
|
@ -32,6 +32,7 @@ impl DbTestContext {
|
||||
> {
|
||||
let customization = Customization {
|
||||
webmaster_email: self.config.webmaster_email.clone(),
|
||||
hostname: self.config.hostname.clone()
|
||||
};
|
||||
|
||||
init_service(create_app(
|
||||
|
@ -21,6 +21,7 @@ pub struct RequestConfig {
|
||||
}
|
||||
|
||||
impl RequestConfig {
|
||||
/// Creates a new [`RequestConfig`] with User as [`Role::Staff`] and [`Function::Posten`] in Area 1.
|
||||
pub fn new(uri: &str) -> Self {
|
||||
Self {
|
||||
uri: uri.to_string(),
|
||||
|
58
web/src/utils/validation/email.rs
Normal file
58
web/src/utils/validation/email.rs
Normal file
@ -0,0 +1,58 @@
|
||||
// great inspiration taken from https://github.com/jprochazk/garde/blob/main/garde/src/rules/email.rs
|
||||
use regex::Regex;
|
||||
|
||||
use super::AsyncValidateError;
|
||||
|
||||
pub fn email_is_valid(email: &str) -> Result<(), AsyncValidateError> {
|
||||
if email.is_empty() {
|
||||
return Err(AsyncValidateError::new("E-Mail ist leer!"));
|
||||
}
|
||||
|
||||
let (user, domain) = email
|
||||
.split_once('@')
|
||||
.ok_or(AsyncValidateError::new("E-Mail enthält kein '@'!"))?;
|
||||
|
||||
if user.len() > 64 {
|
||||
return Err(AsyncValidateError::new("Nutzerteil der E-Mail zu lang!"));
|
||||
}
|
||||
|
||||
let user_re = Regex::new(r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap();
|
||||
|
||||
if !user_re.is_match(user) {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Nutzerteil der E-Mail enthält unerlaubte Zeichen.",
|
||||
));
|
||||
}
|
||||
|
||||
if domain.len() > 255 {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Domainteil der E-Mail ist zu lang.",
|
||||
));
|
||||
}
|
||||
|
||||
let domain_re = Regex::new(
|
||||
r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !domain_re.is_match(domain) {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Domainteil der E-Mail enthält unerlaubte Zeichen!",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn email_validation_works_correctly() {
|
||||
assert!(email_is_valid("abc@example.com").is_ok());
|
||||
assert!(email_is_valid("admin.new@example-domain.de").is_ok());
|
||||
assert!(email_is_valid("admin!new@sub.web.www.example-domain.de").is_ok());
|
||||
|
||||
assert!(email_is_valid("admin.domain.de").is_err());
|
||||
assert!(email_is_valid("admin@web@domain.de").is_err());
|
||||
assert!(email_is_valid("@domain.de").is_err());
|
||||
assert!(email_is_valid("user@").is_err());
|
||||
assert!(email_is_valid("").is_err());
|
||||
}
|
29
web/src/utils/validation/error.rs
Normal file
29
web/src/utils/validation/error.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncValidateError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AsyncValidateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AsyncValidateError {}
|
||||
|
||||
impl AsyncValidateError {
|
||||
pub fn new(message: &str) -> Self {
|
||||
AsyncValidateError {
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for AsyncValidateError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
error!(error = %value, "database error while validation input");
|
||||
AsyncValidateError::new("Datenbankfehler beim Validieren!")
|
||||
}
|
||||
}
|
32
web/src/utils/validation/mod.rs
Normal file
32
web/src/utils/validation/mod.rs
Normal file
@ -0,0 +1,32 @@
|
||||
mod email;
|
||||
mod error;
|
||||
mod r#trait;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
pub use email::email_is_valid;
|
||||
pub use error::AsyncValidateError;
|
||||
pub use r#trait::AsyncValidate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub struct DbContext<'a> {
|
||||
pub pool: &'a PgPool,
|
||||
}
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn new(pool: &'a PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_date_time_lies_before_end_date_time(
|
||||
start: &NaiveDateTime,
|
||||
end: &NaiveDateTime,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if start >= end {
|
||||
return Err(AsyncValidateError::new(
|
||||
"endtime can't lie before starttime",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
8
web/src/utils/validation/trait.rs
Normal file
8
web/src/utils/validation/trait.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use super::AsyncValidateError;
|
||||
|
||||
pub trait AsyncValidate<'a> {
|
||||
type Context: 'a;
|
||||
|
||||
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError>;
|
||||
}
|
||||
|
411
web/static/package-lock.json
generated
411
web/static/package-lock.json
generated
@ -10,13 +10,13 @@
|
||||
"htmx.org": "^1.9.12",
|
||||
"hyperscript.org": "^0.9.14",
|
||||
"sass": "^1.77.8",
|
||||
"sweetalert2-neutral": "^11.14.1-neutral-fix6"
|
||||
"sweetalert2-neutral": "^11.18.0-neutral"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -78,9 +78,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
|
||||
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@ -99,25 +99,25 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.0",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.0",
|
||||
"@parcel/watcher-darwin-x64": "2.5.0",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.0",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.0",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.0",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.0",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.0",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.0",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.0",
|
||||
"@parcel/watcher-win32-arm64": "2.5.0",
|
||||
"@parcel/watcher-win32-ia32": "2.5.0",
|
||||
"@parcel/watcher-win32-x64": "2.5.0"
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz",
|
||||
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -136,9 +136,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz",
|
||||
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -157,9 +157,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz",
|
||||
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -178,9 +178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz",
|
||||
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -199,9 +199,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz",
|
||||
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -220,9 +220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -241,9 +241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz",
|
||||
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -262,9 +262,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -283,9 +283,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz",
|
||||
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -325,9 +325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz",
|
||||
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -346,9 +346,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz",
|
||||
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -367,9 +367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz",
|
||||
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -410,9 +410,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -424,13 +424,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz",
|
||||
"integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==",
|
||||
"version": "24.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz",
|
||||
"integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
@ -609,9 +609,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -622,30 +622,51 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@ -663,9 +684,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
||||
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
|
||||
"version": "4.25.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -683,10 +704,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001669",
|
||||
"electron-to-chromium": "^1.5.41",
|
||||
"node-releases": "^2.0.18",
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@ -703,16 +724,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bulma": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.2.tgz",
|
||||
"integrity": "sha512-D7GnDuF6seb6HkcnRMM9E739QpEY9chDzzeFrHMyEns/EXyDJuQ0XA0KxbBl/B2NTsKSoDomW61jFGFaAxhK5A==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001683",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz",
|
||||
"integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==",
|
||||
"version": "1.0.30001723",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
|
||||
"integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -731,9 +752,9 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -771,9 +792,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.39.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||
"version": "3.43.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz",
|
||||
"integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@ -797,16 +818,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.64",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz",
|
||||
"integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==",
|
||||
"version": "1.5.167",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
|
||||
"integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -818,9 +839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -898,12 +919,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/feather-icons": {
|
||||
"version": "4.29.2",
|
||||
@ -976,9 +1007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1041,9 +1072,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1125,9 +1156,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1152,16 +1183,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -1173,19 +1194,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
|
||||
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -1208,9 +1239,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz",
|
||||
"integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==",
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1229,15 +1260,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
@ -1305,9 +1337,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sweetalert2-neutral": {
|
||||
"version": "11.14.1-neutral-fix6",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2-neutral/-/sweetalert2-neutral-11.14.1-neutral-fix6.tgz",
|
||||
"integrity": "sha512-DpBDnF43AKBtpUZ96uqLsMhezvAG87rXS4Af69e1MP7zfh/8nKPkDeAD3w8oTNl32FUqBcF2d0d0HmAEH2+viw==",
|
||||
"version": "11.18.0-neutral",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2-neutral/-/sweetalert2-neutral-11.18.0-neutral.tgz",
|
||||
"integrity": "sha512-AF+hBm/V850oS/65raRtkQHM0MNe6HKKkrWFfzLLUhmA+3ISjU4IAq6eIBMvu7qM9PKjSTDpY0YS1KRpZM+0kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1315,9 +1347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -1325,14 +1357,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.36.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
|
||||
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
|
||||
"version": "5.42.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz",
|
||||
"integrity": "sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@ -1344,17 +1376,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.10",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
|
||||
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.20",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^3.1.1",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"terser": "^5.26.0"
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
@ -1393,16 +1425,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
||||
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1421,7 +1453,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.0"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
@ -1430,20 +1462,10 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1455,17 +1477,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.96.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz",
|
||||
"integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==",
|
||||
"version": "5.99.9",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz",
|
||||
"integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@webassemblyjs/ast": "^1.12.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.12.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.12.1",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.14.0",
|
||||
"browserslist": "^4.24.0",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
@ -1479,9 +1502,9 @@
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.2.0",
|
||||
"schema-utils": "^4.3.2",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
@ -1502,9 +1525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.2.tgz",
|
||||
"integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -5,7 +5,7 @@
|
||||
"htmx.org": "^1.9.12",
|
||||
"hyperscript.org": "^0.9.14",
|
||||
"sass": "^1.77.8",
|
||||
"sweetalert2-neutral": "^11.14.1-neutral-fix6"
|
||||
"sweetalert2-neutral": "^11.18.0-neutral"
|
||||
},
|
||||
"scripts": {
|
||||
"build-bulma": "sass --load-path=node_modules --no-source-map style.scss dist/style.css"
|
||||
|
@ -44,6 +44,7 @@ $primary: $crimson,
|
||||
@forward "bulma/sass/helpers/flexbox";
|
||||
@forward "bulma/sass/helpers/color";
|
||||
@forward "bulma/sass/helpers/visibility";
|
||||
@forward "bulma/sass/helpers/other";
|
||||
|
||||
// Import the themes so that all CSS variables have a value
|
||||
@forward "bulma/sass/themes";
|
||||
|
@ -9,7 +9,7 @@
|
||||
date|fmt_date(DayMonthYear) }}</h1>
|
||||
|
||||
<input type="hidden" name="startdate" value="{{ date }}">
|
||||
<input type="hidden" name="enddate" value="{{ date }}" id="enddate">
|
||||
<input type="hidden" name="enddate" value="{{ enddate.as_ref().unwrap_or(date) }}" id="enddate">
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
@ -35,6 +35,7 @@
|
||||
then if (value of the previous <input/>) is greater than (value of me)
|
||||
then set the value of #enddate to "{{ datetomorrow }}"
|
||||
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed
|
||||
then set checked of #radionextday to true
|
||||
end' />
|
||||
</div>
|
||||
</div>
|
||||
@ -55,7 +56,7 @@
|
||||
am selben Tag
|
||||
</label>
|
||||
<label class="radio ml-3">
|
||||
<input type="radio" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
||||
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
||||
_='on click set the value of #enddate to "{{ datetomorrow }}"
|
||||
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed'>
|
||||
am Tag darauf
|
||||
@ -80,7 +81,7 @@
|
||||
<use href="/static/feather-sprite.svg#info" />
|
||||
</svg>
|
||||
{% let start_time = start.unwrap_or(NaiveTime::from_hms_opt(10, 0, 0).unwrap()) %}
|
||||
{% let end_time = start.unwrap_or(NaiveTime::from_hms_opt(20, 0, 0).unwrap()) %}
|
||||
{% let end_time = end.unwrap_or(NaiveTime::from_hms_opt(20, 0, 0).unwrap()) %}
|
||||
verfügbar von {{ date|fmt_date(DayMonth) }} <span id="st">{{ start_time|fmt_time(HourMinute) }}</span> Uhr
|
||||
bis <span id="ed">{{ enddate.as_ref().unwrap_or(date)|fmt_date(DayMonth) }}</span>
|
||||
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span> Uhr
|
||||
|
@ -4,6 +4,6 @@
|
||||
|
||||
<p>{{ register_url }}</p>
|
||||
|
||||
<p>Bitte beachte, dass der Link <b>nur 24 Stunden gültig</b> ist.</p>
|
||||
<p>Bitte beachte, dass der Link <b>nur 5 Tage lang gültig</b> ist.</p>
|
||||
|
||||
<p>Viele Grüße</p>
|
||||
|
@ -4,6 +4,6 @@ dein Account für https://{{ hostname }} wurde erstellt. Du musst nur noch ein P
|
||||
|
||||
{{ register_url }}
|
||||
|
||||
Bitte beachte, dass der Link nur 24 Stunden gültig ist.
|
||||
Bitte beachte, dass der Link nur 5 Tage lang gültig ist.
|
||||
|
||||
Viele Grüße
|
||||
|
@ -11,20 +11,28 @@
|
||||
<div class="field">
|
||||
<label class="label">Zeitraum Start</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="start" required value="{{ daterange.0 }}" />
|
||||
<input class="input" type="date" name="start" id="start" required value="{{ daterange.0 }}" />
|
||||
</div>
|
||||
<div class="tags help">
|
||||
<span class="tag">aktueller Monat</span>
|
||||
<span class="tag">nächster Monat</span>
|
||||
<span class="tag">aktuelles Quartal</span>
|
||||
<span class="tag">nächstes Quartal</span>
|
||||
<span class="tag is-link is-light is-clickable"
|
||||
_='on click set the value of #start to "{{ current_month.0 }}" then set the value of #end to "{{ current_month.1 }}"'>
|
||||
aktueller Monat</span>
|
||||
<span class="tag is-link is-light is-clickable"
|
||||
_='on click set the value of #start to "{{ next_month.0 }}" then set the value of #end to "{{ next_month.1 }}"'>
|
||||
nächster Monat</span>
|
||||
<span class="tag is-link is-light is-clickable"
|
||||
_='on click set the value of #start to "{{ current_quarter.0 }}" then set the value of #end to "{{ current_quarter.1 }}"'>
|
||||
aktuelles Quartal</span>
|
||||
<span class="tag is-link is-light is-clickable"
|
||||
_='on click set the value of #start to "{{ next_quarter.0 }}" then set the value of #end to "{{ next_quarter.1 }}"'>
|
||||
nächstes Quartal</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Zeitraum Ende</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="end" required value="{{ daterange.1 }}" />
|
||||
<input class="input" type="date" name="end" id="end" required value="{{ daterange.1 }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li>Vor- und Nachname z.B. <code>Max Mustermann</code></li>
|
||||
<li>E-Mail Adresse z.B. <code>max.mustermann@brasiwa-leipzig.de</code></li>
|
||||
<li>E-Mail Adresse z.B. <code>max.mustermann@{{ hostname }}</code></li>
|
||||
<li>Funktion für Brandsicherheitswachen z.B. <code>Wachhabender</code></li>
|
||||
<li>Brandsicherheitswachbereich z.B. <code>Bereich Ost</code></li>
|
||||
<li>Zeitpunkt deines letzten Logins z.B. <code>2021-06-11 09:40:47</code></li>
|
||||
@ -72,6 +72,9 @@
|
||||
</ul>
|
||||
|
||||
<h1>Copyright</h1>
|
||||
<p>Copyright 2025 Max Hohlfeld</p>
|
||||
<p>Brass ist freie Software nach <a href="https://www.gnu.org/licenses/agpl-3.0.en.html#license-text"
|
||||
target="_blank">GNU AGPLv3</a> Lizenz.</p>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
@ -70,7 +70,7 @@
|
||||
<div class="navbar-item">
|
||||
angemeldet als {{ user.name }}
|
||||
<div class="buttons ml-3">
|
||||
<a class="button is-success" hx-boost="true" href="/profile">
|
||||
<a class="button is-success is-outlined" hx-boost="true" href="/profile">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#user" />
|
||||
</svg>
|
||||
@ -81,8 +81,11 @@
|
||||
<use href="/static/feather-sprite.svg#moon" />
|
||||
</svg>
|
||||
</button>
|
||||
<a href="/logout" class="button is-light">
|
||||
Abmelden
|
||||
<a href="/logout" class="button is-danger is-outlined">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#log-out" />
|
||||
</svg>
|
||||
<span>Abmelden</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,13 +3,10 @@
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% if id.is_some() %}
|
||||
<form method="post" action="/users/edit/{{ id.unwrap() }}">
|
||||
<h1 class="title">Nutzer '{{ name.as_ref().unwrap() }}' bearbeiten</h1>
|
||||
{% else %}
|
||||
<form method="post" action="/users/new">
|
||||
<h1 class="title">Neuen Nutzer anlegen</h1>
|
||||
{% endif %}
|
||||
<form hx-post="/users/{% if let Some(id) = id %}edit/{{ id }}{% else %}new{% endif %}" hx-target-422="find p">
|
||||
<h1 class="title">
|
||||
{% if let Some(name) = name %}Nutzer '{{ name }}' bearbeiten{% else %}Neuen Nutzer anlegen{% endif %}
|
||||
</h1>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
@ -19,8 +16,9 @@
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="email" placeholder="max.mustermann@brasiwa-leipzig.de" {{
|
||||
email|insert_value|safe }} required />
|
||||
email|insert_value|safe }} required _="on input put '' into the next <p/>" />
|
||||
</div>
|
||||
<p class="help is-danger"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user