Compare commits
2 Commits
c74716cc3a
...
2aa3cf2c2b
Author | SHA1 | Date | |
---|---|---|---|
2aa3cf2c2b | |||
bddeaefe4f |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);\n ",
|
||||
"query": "\n INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, fuehrungsassistentRequired, amountOfPosten, clothing, note)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@ -18,5 +18,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b259a464a99501cb60551791af069f662da9fed90243ac4a42d1cf1020b614d3"
|
||||
"hash": "1c9bb8c34c501a3a24708b14d93a3889a11c37dfab085cd777db859ff28b519b"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, voluntaryFuehrungsassistent = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10;\n ",
|
||||
"query": "\n UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, fuehrungsassistentRequired = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@ -19,5 +19,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ee7abc2204854f5934e683d732493037de6b72d3311f10fa0cf74b7ce7ae11bf"
|
||||
"hash": "1ff3f7006fd5594586c2c5c8604e92cf350c1649c02d8355c9ed600385f6b096"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n event.starttimestamp,\n event.endtimestamp,\n event.amountofposten,\n event.voluntaryfuehrungsassistent,\n event.voluntarywachhabender,\n location.name AS locationName,\n event.name AS eventName,\n array (\n SELECT\n row (user_.name, assignment.function) ::simpleAssignment\n FROM\n assignment\n JOIN availability on\n assignment.availabilityid = availability.id\n JOIN user_ on\n availability.userid = user_.id\n WHERE\n assignment.eventId = event.id) AS \"assignments: Vec<SimpleAssignment>\",\n array (\n SELECT\n vehicle.station || ' ' || vehicle.radiocallname\n FROM\n vehicleassignment\n JOIN vehicle on\n vehicleassignment.vehicleId = vehicle.id\n WHERE\n vehicleassignment.eventId = event.id) AS vehicles\n FROM\n event\n JOIN location on\n event.locationId = location.id\n WHERE\n event.starttimestamp::date >= $1\n AND event.starttimestamp::date <= $2\n AND location.areaId = $3\n ORDER BY\n event.starttimestamp\n",
|
||||
"query": "SELECT\n event.starttimestamp,\n event.endtimestamp,\n event.amountofposten,\n event.fuehrungsassistentrequired,\n event.voluntarywachhabender,\n location.name AS locationName,\n event.name AS eventName,\n array (\n SELECT\n row (user_.name, assignment.function) ::simpleAssignment\n FROM\n assignment\n JOIN availability on\n assignment.availabilityid = availability.id\n JOIN user_ on\n availability.userid = user_.id\n WHERE\n assignment.eventId = event.id) AS \"assignments: Vec<SimpleAssignment>\",\n array (\n SELECT\n vehicle.station || ' ' || vehicle.radiocallname\n FROM\n vehicleassignment\n JOIN vehicle on\n vehicleassignment.vehicleId = vehicle.id\n WHERE\n vehicleassignment.eventId = event.id) AS vehicles\n FROM\n event\n JOIN location on\n event.locationId = location.id\n WHERE\n event.starttimestamp::date >= $1\n AND event.starttimestamp::date <= $2\n AND location.areaId = $3\n ORDER BY\n event.starttimestamp\n",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "voluntaryfuehrungsassistent",
|
||||
"name": "fuehrungsassistentrequired",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
@ -102,5 +102,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8a9f517c18b13e6cbf47404d69c2e1c59660250aa5d1074d4da9c0589d0a6bb3"
|
||||
"hash": "2975aea80c391b613e9c12ae5f2802806688720e03f0df81b6dc5c79c7642133"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.voluntaryFuehrungsassistent,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE starttimestamp::date = $1\n AND location.areaId = $2;\n ",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.fuehrungsassistentRequired,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE starttimestamp::date = $1\n AND location.areaId = $2;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "voluntaryfuehrungsassistent",
|
||||
"name": "fuehrungsassistentrequired",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
@ -109,5 +109,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4ceb2c7e3d921c2718e75ba131eee1706e008b783deb687e04eba08f4b919ac8"
|
||||
"hash": "32d009eb1d61484466cfbb8bc77cd3d965a1e52c73f5b9d92df4e956286b06a6"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.voluntaryFuehrungsassistent,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE starttimestamp::date >= $1\n AND starttimestamp::date <= $2\n AND location.areaId = $3\n ORDER BY event.starttimestamp;\n ",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.fuehrungsassistentRequired,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE starttimestamp::date >= $1\n AND starttimestamp::date <= $2\n AND location.areaId = $3\n ORDER BY event.starttimestamp;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "voluntaryfuehrungsassistent",
|
||||
"name": "fuehrungsassistentrequired",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
@ -110,5 +110,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "65367483e39e07cd0aa142f9bb76c7a5d6dd0611e6b41edd5a593c9f955b5d04"
|
||||
"hash": "fb4edba6e7bffb9f61c9d825c57f445d0dd79705531c367277b19bbecedff3e3"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.voluntaryFuehrungsassistent,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE event.id = $1;\n ",
|
||||
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.fuehrungsassistentRequired,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE event.id = $1;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "voluntaryfuehrungsassistent",
|
||||
"name": "fuehrungsassistentrequired",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
@ -108,5 +108,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6dc18993de451d1e0aa4080f00f46ce3339e020922b9ef130c4289b080a2af7d"
|
||||
"hash": "ffb7d4117a008368ab3f1c63118b0fea13eda90942246e65d1bd803dbc5585b1"
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE event
|
||||
RENAME COLUMN voluntaryFuehrungsassistent TO fuehrungsassistentRequired;
|
@ -2,7 +2,7 @@ SELECT
|
||||
event.starttimestamp,
|
||||
event.endtimestamp,
|
||||
event.amountofposten,
|
||||
event.voluntaryfuehrungsassistent,
|
||||
event.fuehrungsassistentrequired,
|
||||
event.voluntarywachhabender,
|
||||
location.name AS locationName,
|
||||
event.name AS eventName,
|
||||
|
@ -128,7 +128,7 @@ async fn event_has_free_slot_for_function(
|
||||
if match *value {
|
||||
Function::Posten => assignments_with_function >= event.amount_of_posten as usize,
|
||||
Function::Fuehrungsassistent => {
|
||||
event.voluntary_fuehrungsassistent && assignments_with_function >= 1
|
||||
event.fuehrungsassistent_required && assignments_with_function >= 1
|
||||
}
|
||||
Function::Wachhabender => event.voluntary_wachhabender && assignments_with_function >= 1,
|
||||
} {
|
||||
|
@ -295,15 +295,13 @@ impl Availability {
|
||||
.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(),
|
||||
})
|
||||
let adjacent_avaialability = records.first().map(|r| 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)
|
||||
|
@ -26,17 +26,17 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
|
||||
&self,
|
||||
context: &'a Self::Context,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let mut existing_availabilities =
|
||||
Availability::read_all_by_user_and_date(context.pool, context.user_id, &self.time.0.date())
|
||||
.await?;
|
||||
let mut existing_availabilities = Availability::read_all_by_user_and_date(
|
||||
context.pool,
|
||||
context.user_id,
|
||||
&self.time.0.date(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||
|
||||
if let Some(availability) = context.availability {
|
||||
existing_availabilities = existing_availabilities
|
||||
.into_iter()
|
||||
.filter(|a| a.id != availability)
|
||||
.collect();
|
||||
existing_availabilities.retain(|a| a.id != availability);
|
||||
|
||||
time_is_not_already_assigned(&self.time, availability, context.pool).await?;
|
||||
}
|
||||
@ -51,7 +51,7 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
|
||||
|
||||
fn time_is_not_already_made_available(
|
||||
(start, end): &(NaiveDateTime, NaiveDateTime),
|
||||
existing_availabilities: &Vec<Availability>,
|
||||
existing_availabilities: &[Availability],
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let free_slots = find_free_date_time_slots(existing_availabilities);
|
||||
|
||||
@ -65,7 +65,7 @@ fn time_is_not_already_made_available(
|
||||
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= *end && s.1 >= *end);
|
||||
let is_already_present_as_is = existing_availabilities
|
||||
.iter()
|
||||
.any(|a| a.start == *start && a.end == a.end);
|
||||
.any(|a| a.start == *start && a.end == *end);
|
||||
|
||||
if !free_block_found_for_start || !free_block_found_for_end || is_already_present_as_is {
|
||||
return Err(AsyncValidateError::new(
|
||||
|
@ -12,7 +12,7 @@ pub struct Event {
|
||||
pub location_id: i32,
|
||||
pub location: Option<Location>,
|
||||
pub voluntary_wachhabender: bool,
|
||||
pub voluntary_fuehrungsassistent: bool,
|
||||
pub fuehrungsassistent_required: bool,
|
||||
pub amount_of_posten: i16,
|
||||
pub clothing: Clothing,
|
||||
pub canceled: bool,
|
||||
@ -22,10 +22,10 @@ pub struct Event {
|
||||
impl Event {
|
||||
pub async fn create(pool: &PgPool, changeset: EventChangeset) -> Result<()> {
|
||||
query!(r#"
|
||||
INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note)
|
||||
INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, fuehrungsassistentRequired, amountOfPosten, clothing, note)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
||||
"#,
|
||||
changeset.time.0.and_utc(), changeset.time.1.and_utc(), changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note).execute(pool).await?;
|
||||
changeset.time.0.and_utc(), changeset.time.1.and_utc(), changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.fuehrungsassistent_required, changeset.amount_of_posten, changeset.clothing, changeset.note).execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -44,7 +44,7 @@ impl Event {
|
||||
event.name,
|
||||
event.locationId,
|
||||
event.voluntaryWachhabender,
|
||||
event.voluntaryFuehrungsassistent,
|
||||
event.fuehrungsassistentRequired,
|
||||
event.amountOfPosten,
|
||||
event.clothing,
|
||||
event.canceled,
|
||||
@ -81,7 +81,7 @@ impl Event {
|
||||
area: None,
|
||||
}),
|
||||
voluntary_wachhabender: record.voluntarywachhabender,
|
||||
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: record.fuehrungsassistentrequired,
|
||||
amount_of_posten: record.amountofposten,
|
||||
clothing: Clothing {
|
||||
id: record.clothingid,
|
||||
@ -109,7 +109,7 @@ impl Event {
|
||||
event.name,
|
||||
event.locationId,
|
||||
event.voluntaryWachhabender,
|
||||
event.voluntaryFuehrungsassistent,
|
||||
event.fuehrungsassistentRequired,
|
||||
event.amountOfPosten,
|
||||
event.clothing,
|
||||
event.canceled,
|
||||
@ -149,7 +149,7 @@ impl Event {
|
||||
area: None,
|
||||
}),
|
||||
voluntary_wachhabender: record.voluntarywachhabender,
|
||||
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: record.fuehrungsassistentrequired,
|
||||
amount_of_posten: record.amountofposten,
|
||||
clothing: Clothing {
|
||||
id: record.clothingid,
|
||||
@ -173,7 +173,7 @@ impl Event {
|
||||
event.name,
|
||||
event.locationId,
|
||||
event.voluntaryWachhabender,
|
||||
event.voluntaryFuehrungsassistent,
|
||||
event.fuehrungsassistentRequired,
|
||||
event.amountOfPosten,
|
||||
event.clothing,
|
||||
event.canceled,
|
||||
@ -206,7 +206,7 @@ impl Event {
|
||||
area: None,
|
||||
}),
|
||||
voluntary_wachhabender: record.voluntarywachhabender,
|
||||
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: record.fuehrungsassistentrequired,
|
||||
amount_of_posten: record.amountofposten,
|
||||
clothing: Clothing {
|
||||
id: record.clothingid,
|
||||
@ -221,14 +221,14 @@ impl Event {
|
||||
|
||||
pub async fn update(pool: &PgPool, id: i32, changeset: EventChangeset) -> Result<()> {
|
||||
query!(r#"
|
||||
UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, voluntaryFuehrungsassistent = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10;
|
||||
UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, fuehrungsassistentRequired = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10;
|
||||
"#,
|
||||
changeset.time.0.and_utc(),
|
||||
changeset.time.1.and_utc(),
|
||||
changeset.name,
|
||||
changeset.location_id,
|
||||
changeset.voluntary_wachhabender,
|
||||
changeset.voluntary_fuehrungsassistent,
|
||||
changeset.fuehrungsassistent_required,
|
||||
changeset.amount_of_posten,
|
||||
changeset.clothing,
|
||||
changeset.note, id)
|
||||
|
@ -23,7 +23,7 @@ pub struct EventChangeset {
|
||||
pub name: String,
|
||||
pub location_id: i32,
|
||||
pub voluntary_wachhabender: bool,
|
||||
pub voluntary_fuehrungsassistent: bool,
|
||||
pub fuehrungsassistent_required: bool,
|
||||
pub amount_of_posten: i16,
|
||||
pub clothing: i32,
|
||||
pub note: Option<String>,
|
||||
@ -69,7 +69,7 @@ impl<'a> AsyncValidate<'a> for EventChangeset {
|
||||
date_unchanged_if_edit(&self.time, &event.start.date())?;
|
||||
can_unset_wachhabender(&self.voluntary_wachhabender, &assignments_for_event)?;
|
||||
can_unset_fuehrungsassistent(
|
||||
&self.voluntary_fuehrungsassistent,
|
||||
&self.fuehrungsassistent_required,
|
||||
&assignments_for_event,
|
||||
)?;
|
||||
if location.area_id != event.location.unwrap().area_id {
|
||||
@ -117,7 +117,7 @@ fn date_unchanged_if_edit(
|
||||
async fn time_can_be_extended_if_edit(
|
||||
time: &(NaiveDateTime, NaiveDateTime),
|
||||
event: &Event,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
assignments_for_event: &[Assignment],
|
||||
pool: &PgPool,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let start = event.start.date();
|
||||
@ -197,7 +197,7 @@ async fn time_can_be_extended_if_edit(
|
||||
|
||||
fn can_unset_fuehrungsassistent(
|
||||
fuehrungsassistent_required: &bool,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
assignments_for_event: &[Assignment],
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if !*fuehrungsassistent_required
|
||||
&& assignments_for_event
|
||||
@ -214,7 +214,7 @@ fn can_unset_fuehrungsassistent(
|
||||
|
||||
fn can_unset_wachhabender(
|
||||
voluntary_wachhabender: &bool,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
assignments_for_event: &[Assignment],
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if !*voluntary_wachhabender
|
||||
&& assignments_for_event
|
||||
@ -237,7 +237,7 @@ impl EventChangeset {
|
||||
name: Faker.fake(),
|
||||
location_id: 1,
|
||||
voluntary_wachhabender: true,
|
||||
voluntary_fuehrungsassistent: true,
|
||||
fuehrungsassistent_required: true,
|
||||
amount_of_posten: 5,
|
||||
clothing: 1,
|
||||
note: None,
|
||||
|
@ -11,7 +11,7 @@ pub struct ExportEventRow {
|
||||
pub start_timestamp: NaiveDateTime,
|
||||
pub end_timestamp: NaiveDateTime,
|
||||
pub amount_of_posten: i16,
|
||||
pub voluntary_fuehrungsassistent: bool,
|
||||
pub fuehrungsassistent_required: bool,
|
||||
pub voluntary_wachhabender: bool,
|
||||
pub location_name: String,
|
||||
pub event_name: String,
|
||||
@ -53,12 +53,12 @@ impl ExportEventRow {
|
||||
start_timestamp: r.starttimestamp.naive_utc(),
|
||||
end_timestamp: r.endtimestamp.naive_utc(),
|
||||
amount_of_posten: r.amountofposten,
|
||||
voluntary_fuehrungsassistent: r.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: r.fuehrungsassistentrequired,
|
||||
voluntary_wachhabender: r.voluntarywachhabender,
|
||||
location_name: r.locationname,
|
||||
event_name: r.eventname,
|
||||
assignments: r.assignments.unwrap_or(Vec::new()),
|
||||
vehicles: r.vehicles.unwrap_or(Vec::new()),
|
||||
assignments: r.assignments.unwrap_or_default(),
|
||||
vehicles: r.vehicles.unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -152,7 +152,7 @@ impl User {
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(record.and_then(|r| Some(r.id)))
|
||||
Ok(record.map(|r| r.id))
|
||||
}
|
||||
|
||||
pub async fn read_all(pool: &PgPool) -> Result<Vec<User>> {
|
||||
|
@ -19,7 +19,7 @@ impl Display for UserFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iterator = self.0.iter().peekable();
|
||||
while let Some(p) = iterator.next() {
|
||||
write!(f, "{}", p.to_string())?;
|
||||
write!(f, "{p}")?;
|
||||
if iterator.peek().is_some() {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ snapshot_kind: text
|
||||
|
||||
<input type="hidden" name="date" value="2025-01-01">
|
||||
<input type="hidden" name="voluntarywachhabender" id="voluntarywachhabender" value="false">
|
||||
<input type="hidden" name="voluntaryfuehrungsassistent" id="voluntaryfuehrungsassistent"
|
||||
<input type="hidden" name="fuehrungsassistentrequired" id="fuehrungsassistentrequired"
|
||||
value="false">
|
||||
|
||||
|
||||
@ -133,7 +133,7 @@ snapshot_kind: text
|
||||
<label class="checkbox">
|
||||
<input class="checkbox" type="checkbox"
|
||||
|
||||
_="on click put (the value of #voluntaryfuehrungsassistent) as inverseBool into the value of #voluntaryfuehrungsassistent">
|
||||
_="on click put (the value of #fuehrungsassistentrequired) as inverseBool into the value of #fuehrungsassistentrequired">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -51,7 +51,7 @@ pub async fn get(
|
||||
name: Some(event.name),
|
||||
location: Some(event.location_id),
|
||||
voluntary_wachhabender: event.voluntary_wachhabender,
|
||||
voluntary_fuehrungsassistent: event.voluntary_fuehrungsassistent,
|
||||
fuehrungsassistent_required: event.fuehrungsassistent_required,
|
||||
amount_of_posten: Some(event.amount_of_posten),
|
||||
clothing: Some(event.clothing.id),
|
||||
clothing_options,
|
||||
@ -86,7 +86,7 @@ mod tests {
|
||||
),
|
||||
name: "Vorstellung".to_string(),
|
||||
location_id: 1,
|
||||
voluntary_fuehrungsassistent: false,
|
||||
fuehrungsassistent_required: false,
|
||||
voluntary_wachhabender: false,
|
||||
amount_of_posten: 2,
|
||||
clothing: 1,
|
||||
|
@ -41,7 +41,7 @@ pub async fn get(
|
||||
name: None,
|
||||
location: None,
|
||||
voluntary_wachhabender: false,
|
||||
voluntary_fuehrungsassistent: false,
|
||||
fuehrungsassistent_required: false,
|
||||
amount_of_posten: None,
|
||||
clothing: None,
|
||||
clothing_options,
|
||||
|
@ -30,7 +30,7 @@ pub struct NewOrEditEventTemplate {
|
||||
name: Option<String>,
|
||||
location: Option<i32>,
|
||||
voluntary_wachhabender: bool,
|
||||
voluntary_fuehrungsassistent: bool,
|
||||
fuehrungsassistent_required: bool,
|
||||
amount_of_posten: Option<i16>,
|
||||
clothing: Option<i32>,
|
||||
clothing_options: Vec<Clothing>,
|
||||
@ -50,7 +50,7 @@ pub struct NewOrEditEventForm {
|
||||
end: NaiveDateTime,
|
||||
location: i32,
|
||||
voluntarywachhabender: bool,
|
||||
voluntaryfuehrungsassistent: bool,
|
||||
fuehrungsassistentrequired: bool,
|
||||
amount: i16,
|
||||
clothing: i32,
|
||||
note: Option<String>,
|
||||
@ -60,13 +60,15 @@ mod short_date_time_format {
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{self, Deserialize, Deserializer};
|
||||
|
||||
use crate::utils::DateTimeFormat;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
const FORMAT: &'static str = "%Y-%m-%dT%H:%M";
|
||||
let format = DateTimeFormat::YearMonthDayTHourMinute.into();
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let dt = NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?;
|
||||
let dt = NaiveDateTime::parse_from_str(&s, format).map_err(serde::de::Error::custom)?;
|
||||
Ok(dt)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ pub async fn post(
|
||||
.clone()
|
||||
.and_then(|n| if !n.is_empty() { Some(n) } else { None }),
|
||||
voluntary_wachhabender: form.voluntarywachhabender,
|
||||
voluntary_fuehrungsassistent: form.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: form.fuehrungsassistentrequired,
|
||||
};
|
||||
|
||||
let context = EventContext {
|
||||
|
@ -31,7 +31,7 @@ pub async fn post(
|
||||
.clone()
|
||||
.and_then(|n| if !n.is_empty() { Some(n) } else { None }),
|
||||
voluntary_wachhabender: form.voluntarywachhabender,
|
||||
voluntary_fuehrungsassistent: form.voluntaryfuehrungsassistent,
|
||||
fuehrungsassistent_required: form.fuehrungsassistentrequired,
|
||||
};
|
||||
|
||||
let event_context = EventContext {
|
||||
|
@ -61,8 +61,12 @@ pub async fn get(
|
||||
user.area_id
|
||||
};
|
||||
|
||||
let availabilities =
|
||||
Availability::read_all_by_daterange_and_area_including_user_for_export(pool.get_ref(), (start_date, end_date), area_id).await?;
|
||||
let availabilities = Availability::read_all_by_daterange_and_area_including_user_for_export(
|
||||
pool.get_ref(),
|
||||
(start_date, end_date),
|
||||
area_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let export_availabilities = availabilities
|
||||
.into_iter()
|
||||
@ -73,7 +77,7 @@ pub async fn get(
|
||||
start: a.start,
|
||||
end: a.end,
|
||||
assigned: false,
|
||||
comment: a.comment.unwrap_or(String::new()),
|
||||
comment: a.comment.unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -55,8 +55,8 @@ fn read(rows: Vec<ExportEventRow>) -> Vec<EventExportEntry> {
|
||||
hours: (r.end_timestamp - r.start_timestamp).as_seconds_f32() / 3600.0 + 1.0,
|
||||
location: r.location_name.to_string(),
|
||||
name: r.event_name.to_string(),
|
||||
assigned_name: n.and_then(|s| Some(s.to_string())),
|
||||
assigned_function: f.and_then(|s| Some(s.to_string())),
|
||||
assigned_name: n.map(|s| s.to_string()),
|
||||
assigned_function: f.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
if let Some(assigned_wh) = r
|
||||
@ -87,7 +87,7 @@ fn read(rows: Vec<ExportEventRow>) -> Vec<EventExportEntry> {
|
||||
Some(&assigned_fuass.name),
|
||||
Some(&assigned_fuass.function.short_display()),
|
||||
));
|
||||
} else if r.voluntary_fuehrungsassistent {
|
||||
} else if r.fuehrungsassistent_required {
|
||||
entries.push(create_new_entry(
|
||||
None,
|
||||
Some(&Function::Fuehrungsassistent.short_display()),
|
||||
@ -182,11 +182,11 @@ pub async fn get(
|
||||
|
||||
let buffer = workbook.save_to_buffer().unwrap();
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
.insert_header((
|
||||
CONTENT_DISPOSITION,
|
||||
ContentDisposition::attachment("export.xlsx"),
|
||||
))
|
||||
.body(buffer));
|
||||
.body(buffer))
|
||||
}
|
||||
|
@ -34,7 +34,10 @@ pub async fn get(
|
||||
}
|
||||
|
||||
if let Some(token) = &query.token {
|
||||
if let Some(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await? {
|
||||
if PasswordReset::does_token_exist(pool.get_ref(), token)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
let template = ResetPasswordTemplate {
|
||||
token,
|
||||
title: "Brass - Passwort zurücksetzen",
|
||||
|
@ -51,7 +51,10 @@ pub async fn post_new(
|
||||
area_id,
|
||||
};
|
||||
|
||||
if let Some(_) = User::exists(pool.get_ref(), &changeset.email).await? {
|
||||
if User::exists(pool.get_ref(), &changeset.email)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(HttpResponse::UnprocessableEntity()
|
||||
.body("email: an user already exists with the same email"));
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ async fn post(
|
||||
.await?;
|
||||
}
|
||||
|
||||
return Ok(HttpResponse::Ok().body("E-Mail versandt!"));
|
||||
Ok(HttpResponse::Ok().body("E-Mail versandt!"))
|
||||
}
|
||||
Either::Right(form) => {
|
||||
let is_dry = header.is_some_and_equal("password-strength");
|
||||
@ -83,7 +83,7 @@ async fn post(
|
||||
)
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,12 +119,10 @@ pub fn fmt_time(v: &NaiveTime, format: DateTimeFormat) -> askama::Result<String>
|
||||
}
|
||||
|
||||
fn escape_html(string: String) -> String {
|
||||
let s = string
|
||||
string
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'");
|
||||
|
||||
s
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ fn build(
|
||||
|
||||
let sender_mailbox = Mailbox::new(
|
||||
Some("noreply".to_string()),
|
||||
Address::new("noreply", &hostname)?,
|
||||
Address::new("noreply", hostname)?,
|
||||
);
|
||||
|
||||
let message = Message::builder()
|
||||
|
@ -62,7 +62,7 @@ fn build(
|
||||
|
||||
let sender_mailbox = Mailbox::new(
|
||||
Some("noreply".to_string()),
|
||||
Address::new("noreply", &hostname)?,
|
||||
Address::new("noreply", hostname)?,
|
||||
);
|
||||
|
||||
let message = Message::builder()
|
||||
|
@ -28,8 +28,8 @@ pub enum ApplicationError {
|
||||
impl actix_web::error::ResponseError for ApplicationError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ApplicationError::UnsupportedEnumValue { .. } => StatusCode::BAD_REQUEST,
|
||||
ApplicationError::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
|
||||
ApplicationError::UnsupportedEnumValue(_) => StatusCode::BAD_REQUEST,
|
||||
ApplicationError::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApplicationError::EnvVariable(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApplicationError::EmailAdress(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
@ -76,7 +76,7 @@ pub async fn generate_status_whether_staff_is_required(
|
||||
.count()
|
||||
< event.amount_of_posten as usize;
|
||||
|
||||
let further_fuehrungsassistent_required = event.voluntary_fuehrungsassistent
|
||||
let further_fuehrungsassistent_required = event.fuehrungsassistent_required
|
||||
&& existing_assignments_for_event
|
||||
.iter()
|
||||
.all(|a| a.function != Function::Fuehrungsassistent);
|
||||
|
@ -20,7 +20,7 @@ impl Header for HtmxTargetHeader {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
return Ok(self::HtmxTargetHeader(Some(line.to_string())));
|
||||
Ok(self::HtmxTargetHeader(Some(line.to_string())))
|
||||
} else {
|
||||
Ok(self::HtmxTargetHeader(None))
|
||||
}
|
||||
@ -33,7 +33,7 @@ impl TryIntoHeaderValue for HtmxTargetHeader {
|
||||
#[inline]
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
if let Some(s) = self.0 {
|
||||
return s.try_into_value();
|
||||
s.try_into_value()
|
||||
} else {
|
||||
HeaderValue::from_str("")
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use chrono::{NaiveDate, Utc};
|
||||
pub fn get_return_url_for_date(date: &NaiveDate) -> String {
|
||||
let today = Utc::now().date_naive();
|
||||
if date == &today {
|
||||
return String::from("/");
|
||||
return String::from("/calendar");
|
||||
}
|
||||
|
||||
format!("/calendar?date={}", date)
|
||||
|
195
web/src/utils/password_change/command.rs
Normal file
195
web/src/utils/password_change/command.rs
Normal file
@ -0,0 +1,195 @@
|
||||
use actix_web::HttpResponse;
|
||||
use maud::html;
|
||||
use sqlx::PgPool;
|
||||
use zxcvbn::{
|
||||
feedback::{Suggestion, Warning},
|
||||
zxcvbn, Entropy, Score,
|
||||
};
|
||||
|
||||
use crate::utils::{
|
||||
auth::{generate_salt_and_hash_plain_password, hash_plain_password_with_salt},
|
||||
ApplicationError,
|
||||
};
|
||||
use brass_db::{models::User, Token};
|
||||
|
||||
use super::PasswordChangeError;
|
||||
|
||||
pub struct PasswordChange<'a, T>
|
||||
where
|
||||
T: Token,
|
||||
{
|
||||
pub(super) pool: &'a PgPool,
|
||||
pub(super) user_id: i32,
|
||||
pub(super) password: &'a str,
|
||||
pub(super) password_retyped: &'a str,
|
||||
pub(super) token: Option<T>,
|
||||
pub(super) current_password: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<T> PasswordChange<'_, T>
|
||||
where
|
||||
T: Token,
|
||||
{
|
||||
/// should be called after password input has changed to hint the user of any input related problems
|
||||
pub async fn validate_for_input(&self) -> Result<HttpResponse, PasswordChangeError> {
|
||||
if self.password.chars().count() > 256 {
|
||||
return Err(PasswordChangeError {
|
||||
message: html! {
|
||||
div id="password-strength" class="mb-3 help content is-danger"{
|
||||
"Password darf nicht länger als 256 Zeichen sein."
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// unwrap is safe, as either a token is present (tokens always require a userid present) or the current password is set (which requires a existing user)
|
||||
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
|
||||
let mut split_names: Vec<&str> = user.name.as_str().split_whitespace().collect();
|
||||
let mut user_inputs = vec![user.email.as_str()];
|
||||
user_inputs.append(&mut split_names);
|
||||
let entropy = zxcvbn(self.password, &user_inputs);
|
||||
|
||||
if entropy.score() < Score::Three {
|
||||
let message = generate_help_message_for_entropy(&entropy);
|
||||
return Err(PasswordChangeError { message });
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().body(
|
||||
html! {
|
||||
div id="password-strength" class="mb-3 help content is-success" {
|
||||
@if entropy.score() == Score::Three {
|
||||
"Sicheres Passwort."
|
||||
} @else {
|
||||
"Sehr sicheres Passwort."
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// should be called after the form is fully filled and submit is clicked
|
||||
pub async fn validate(&self) -> Result<(), PasswordChangeError> {
|
||||
self.validate_for_input().await?;
|
||||
|
||||
if let Some(current_password) = self.current_password {
|
||||
// unwraps are safe, as login only works with password and salt and this call site requires login
|
||||
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
|
||||
let hash = user.password.as_ref().unwrap();
|
||||
let salt = user.salt.as_ref().unwrap();
|
||||
|
||||
if hash != &hash_plain_password_with_salt(current_password, salt)? {
|
||||
return Err(PasswordChangeError {
|
||||
message: "Aktuelles Passwort ist falsch.".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.password != self.password_retyped {
|
||||
return Err(PasswordChangeError {
|
||||
message: "Passwörter stimmen nicht überein.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// commits the password change to the database
|
||||
pub async fn commit(&self) -> Result<(), ApplicationError> {
|
||||
let (hash, salt) = generate_salt_and_hash_plain_password(self.password).unwrap();
|
||||
|
||||
User::update_password(self.pool, self.user_id, &hash, &salt).await?;
|
||||
|
||||
if let Some(token) = &self.token {
|
||||
token.delete(self.pool).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_help_message_for_entropy(entropy: &Entropy) -> String {
|
||||
let feedback = entropy.feedback().unwrap();
|
||||
|
||||
let warning = match feedback.warning() {
|
||||
Some(Warning::StraightRowsOfKeysAreEasyToGuess) => {
|
||||
"Gerade Linien von Tasten auf der Tastatur sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::ShortKeyboardPatternsAreEasyToGuess) => {
|
||||
"Kurze Tastaturmuster sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RepeatsLikeAaaAreEasyToGuess) => {
|
||||
"Sich wiederholende Zeichen wie 'aaa' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess) => {
|
||||
"Sich wiederholende Zeichenmuster wie 'abcabcabc' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::ThisIsATop10Password) => "Dies ist ein sehr häufig verwendetes Passwort.",
|
||||
Some(Warning::ThisIsATop100Password) => "Dies ist ein häufig verwendetes Passwort.",
|
||||
Some(Warning::ThisIsACommonPassword) => "Dies ist ein oft verwendetes Passwort.",
|
||||
Some(Warning::ThisIsSimilarToACommonlyUsedPassword) => {
|
||||
"Dieses Passwort weist Ähnlichkeit zu anderen, oft verwendeten Passwörtern auf."
|
||||
}
|
||||
Some(Warning::SequencesLikeAbcAreEasyToGuess) => {
|
||||
"Häufige Zeichenfolgen wie 'abc' oder '1234' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RecentYearsAreEasyToGuess) => {
|
||||
"Die jüngsten Jahreszahlen sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::AWordByItselfIsEasyToGuess) => "Einzelne Wörter sind leicht zu erraten.",
|
||||
Some(Warning::DatesAreOftenEasyToGuess) => "Ein Datum ist leicht zu erraten.",
|
||||
Some(Warning::NamesAndSurnamesByThemselvesAreEasyToGuess) => {
|
||||
"Einzelne Namen oder Nachnamen sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::CommonNamesAndSurnamesAreEasyToGuess) => {
|
||||
"Vornamen und Nachnamen sind leicht zu erraten."
|
||||
}
|
||||
_ => "Passwort ist zu schwach.",
|
||||
};
|
||||
|
||||
let vorschlag_text = if feedback.suggestions().len() > 1 {
|
||||
"Vorschläge"
|
||||
} else {
|
||||
"Vorschlag"
|
||||
};
|
||||
|
||||
let suggestions = feedback
|
||||
.suggestions()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
match s {
|
||||
Suggestion::UseAFewWordsAvoidCommonPhrases => "Mehrere Wörter verwenden, aber allgemeine Phrasen vermeiden.",
|
||||
Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => "Es ist möglich, starke Passwörter zu erstellen, ohne Symbole, Zahlen oder Großbuchstaben zu verwenden.",
|
||||
Suggestion::AddAnotherWordOrTwo => "Weitere Wörter, die weniger häufig vorkommen, hinzufügen.",
|
||||
Suggestion::CapitalizationDoesntHelpVeryMuch => "Nicht nur den ersten Buchstaben groß schreiben.",
|
||||
Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => "Einige, aber nicht alle Buchstaben groß schreiben.",
|
||||
Suggestion::ReversedWordsArentMuchHarderToGuess => "Umgekehrte Schreibweise von gebräuchlichen Wörtern vermeiden.",
|
||||
Suggestion::PredictableSubstitutionsDontHelpVeryMuch => "Vorhersehbare Buchstabenersetzungen wie '@' für 'a' vermeiden.",
|
||||
Suggestion::UseALongerKeyboardPatternWithMoreTurns => "Längere Tastaturmuster in unterschiedlicher Tipprichtung verwenden.",
|
||||
Suggestion::AvoidRepeatedWordsAndCharacters => "Wort- und Zeichenwiederholungen vermeiden.",
|
||||
Suggestion::AvoidSequences => "Häufige Zeichenfolgen vermeiden.",
|
||||
Suggestion::AvoidRecentYears => "Die jüngsten Jahreszahlen vermeiden.",
|
||||
Suggestion::AvoidYearsThatAreAssociatedWithYou => "Jahre, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.",
|
||||
Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => "Daten, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.",
|
||||
}
|
||||
|
||||
})
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
html!(
|
||||
{
|
||||
div id="password-strength" class="mb-3 help content is-danger" {
|
||||
p { ( warning )}
|
||||
(vorschlag_text) ":"
|
||||
ul {
|
||||
@for s in &suggestions {
|
||||
li { (s) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.into_string()
|
||||
}
|
30
web/src/utils/password_change/error.rs
Normal file
30
web/src/utils/password_change/error.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{message}")]
|
||||
pub struct PasswordChangeError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl PasswordChangeError {
|
||||
pub fn new(message: &str) -> Self {
|
||||
PasswordChangeError {
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for PasswordChangeError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
error!(error = %value, "database error while validation input");
|
||||
Self::new("Datenbankfehler beim Validieren!")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::password_hash::Error> for PasswordChangeError {
|
||||
fn from(value: argon2::password_hash::Error) -> Self {
|
||||
error!(error = %value, "argon2 hash error while validation input");
|
||||
Self::new("Hashingfehler beim Validieren!")
|
||||
}
|
||||
}
|
@ -1,126 +1,7 @@
|
||||
use maud::html;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
use zxcvbn::{
|
||||
feedback::{Suggestion, Warning},
|
||||
Entropy,
|
||||
};
|
||||
mod builder;
|
||||
mod command;
|
||||
mod error;
|
||||
|
||||
mod password_change;
|
||||
mod password_change_builder;
|
||||
|
||||
pub use password_change::PasswordChange;
|
||||
pub use password_change_builder::PasswordChangeBuilder;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{message}")]
|
||||
pub struct PasswordChangeError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl PasswordChangeError {
|
||||
pub fn new(message: &str) -> Self {
|
||||
PasswordChangeError {
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for PasswordChangeError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
error!(error = %value, "database error while validation input");
|
||||
Self::new("Datenbankfehler beim Validieren!")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::password_hash::Error> for PasswordChangeError {
|
||||
fn from(value: argon2::password_hash::Error) -> Self {
|
||||
error!(error = %value, "argon2 hash error while validation input");
|
||||
Self::new("Hashingfehler beim Validieren!")
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_help_message_for_entropy(entropy: &Entropy) -> String {
|
||||
let feedback = entropy.feedback().unwrap();
|
||||
|
||||
let warning = match feedback.warning() {
|
||||
Some(Warning::StraightRowsOfKeysAreEasyToGuess) => {
|
||||
"Gerade Linien von Tasten auf der Tastatur sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::ShortKeyboardPatternsAreEasyToGuess) => {
|
||||
"Kurze Tastaturmuster sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RepeatsLikeAaaAreEasyToGuess) => {
|
||||
"Sich wiederholende Zeichen wie 'aaa' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess) => {
|
||||
"Sich wiederholende Zeichenmuster wie 'abcabcabc' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::ThisIsATop10Password) => "Dies ist ein sehr häufig verwendetes Passwort.",
|
||||
Some(Warning::ThisIsATop100Password) => "Dies ist ein häufig verwendetes Passwort.",
|
||||
Some(Warning::ThisIsACommonPassword) => "Dies ist ein oft verwendetes Passwort.",
|
||||
Some(Warning::ThisIsSimilarToACommonlyUsedPassword) => {
|
||||
"Dieses Passwort weist Ähnlichkeit zu anderen, oft verwendeten Passwörtern auf."
|
||||
}
|
||||
Some(Warning::SequencesLikeAbcAreEasyToGuess) => {
|
||||
"Häufige Zeichenfolgen wie 'abc' oder '1234' sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::RecentYearsAreEasyToGuess) => {
|
||||
"Die jüngsten Jahreszahlen sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::AWordByItselfIsEasyToGuess) => "Einzelne Wörter sind leicht zu erraten.",
|
||||
Some(Warning::DatesAreOftenEasyToGuess) => "Ein Datum ist leicht zu erraten.",
|
||||
Some(Warning::NamesAndSurnamesByThemselvesAreEasyToGuess) => {
|
||||
"Einzelne Namen oder Nachnamen sind leicht zu erraten."
|
||||
}
|
||||
Some(Warning::CommonNamesAndSurnamesAreEasyToGuess) => {
|
||||
"Vornamen und Nachnamen sind leicht zu erraten."
|
||||
}
|
||||
_ => "Passwort ist zu schwach.",
|
||||
};
|
||||
|
||||
let vorschlag_text = if feedback.suggestions().len() > 1 {
|
||||
"Vorschläge"
|
||||
} else {
|
||||
"Vorschlag"
|
||||
};
|
||||
|
||||
let suggestions = feedback
|
||||
.suggestions()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
match s {
|
||||
Suggestion::UseAFewWordsAvoidCommonPhrases => "Mehrere Wörter verwenden, aber allgemeine Phrasen vermeiden.",
|
||||
Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => "Es ist möglich, starke Passwörter zu erstellen, ohne Symbole, Zahlen oder Großbuchstaben zu verwenden.",
|
||||
Suggestion::AddAnotherWordOrTwo => "Weitere Wörter, die weniger häufig vorkommen, hinzufügen.",
|
||||
Suggestion::CapitalizationDoesntHelpVeryMuch => "Nicht nur den ersten Buchstaben groß schreiben.",
|
||||
Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => "Einige, aber nicht alle Buchstaben groß schreiben.",
|
||||
Suggestion::ReversedWordsArentMuchHarderToGuess => "Umgekehrte Schreibweise von gebräuchlichen Wörtern vermeiden.",
|
||||
Suggestion::PredictableSubstitutionsDontHelpVeryMuch => "Vorhersehbare Buchstabenersetzungen wie '@' für 'a' vermeiden.",
|
||||
Suggestion::UseALongerKeyboardPatternWithMoreTurns => "Längere Tastaturmuster in unterschiedlicher Tipprichtung verwenden.",
|
||||
Suggestion::AvoidRepeatedWordsAndCharacters => "Wort- und Zeichenwiederholungen vermeiden.",
|
||||
Suggestion::AvoidSequences => "Häufige Zeichenfolgen vermeiden.",
|
||||
Suggestion::AvoidRecentYears => "Die jüngsten Jahreszahlen vermeiden.",
|
||||
Suggestion::AvoidYearsThatAreAssociatedWithYou => "Jahre, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.",
|
||||
Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => "Daten, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.",
|
||||
}
|
||||
|
||||
})
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
html!(
|
||||
{
|
||||
div id="password-strength" class="mb-3 help content is-danger" {
|
||||
p { ( warning )}
|
||||
(vorschlag_text) ":"
|
||||
ul {
|
||||
@for s in &suggestions {
|
||||
li { (s) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.into_string()
|
||||
}
|
||||
pub use builder::PasswordChangeBuilder;
|
||||
pub use command::PasswordChange;
|
||||
pub use error::PasswordChangeError;
|
||||
|
@ -1,110 +0,0 @@
|
||||
use actix_web::HttpResponse;
|
||||
use maud::html;
|
||||
use sqlx::PgPool;
|
||||
use zxcvbn::{zxcvbn, Score};
|
||||
|
||||
use crate::utils::{
|
||||
auth::{generate_salt_and_hash_plain_password, hash_plain_password_with_salt},
|
||||
ApplicationError,
|
||||
};
|
||||
use brass_db::{models::User, Token};
|
||||
|
||||
use super::{generate_help_message_for_entropy, PasswordChangeError};
|
||||
|
||||
pub struct PasswordChange<'a, T>
|
||||
where
|
||||
T: Token,
|
||||
{
|
||||
pub(super) pool: &'a PgPool,
|
||||
pub(super) user_id: i32,
|
||||
pub(super) password: &'a str,
|
||||
pub(super) password_retyped: &'a str,
|
||||
pub(super) token: Option<T>,
|
||||
pub(super) current_password: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<T> PasswordChange<'_, T>
|
||||
where
|
||||
T: Token,
|
||||
{
|
||||
/// should be called after password input has changed to hint the user of any input related problems
|
||||
pub async fn validate_for_input(&self) -> Result<HttpResponse, PasswordChangeError> {
|
||||
if self.password.chars().count() > 256 {
|
||||
return Err(PasswordChangeError {
|
||||
message: html! {
|
||||
div id="password-strength" class="mb-3 help content is-danger"{
|
||||
"Password darf nicht länger als 256 Zeichen sein."
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// unwrap is safe, as either a token is present (tokens always require a userid present) or the current password is set (which requires a existing user)
|
||||
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
|
||||
let mut split_names: Vec<&str> = user.name.as_str().split_whitespace().collect();
|
||||
let mut user_inputs = vec![user.email.as_str()];
|
||||
user_inputs.append(&mut split_names);
|
||||
let entropy = zxcvbn(self.password, &user_inputs);
|
||||
|
||||
if entropy.score() < Score::Three {
|
||||
let message = generate_help_message_for_entropy(&entropy);
|
||||
return Err(PasswordChangeError { message }.into());
|
||||
}
|
||||
|
||||
return Ok(HttpResponse::Ok().body(
|
||||
html! {
|
||||
div id="password-strength" class="mb-3 help content is-success" {
|
||||
@if entropy.score() == Score::Three {
|
||||
"Sicheres Passwort."
|
||||
} @else {
|
||||
"Sehr sicheres Passwort."
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// should be called after the form is fully filled and submit is clicked
|
||||
pub async fn validate(&self) -> Result<(), PasswordChangeError> {
|
||||
self.validate_for_input().await?;
|
||||
|
||||
if let Some(current_password) = self.current_password {
|
||||
// unwraps are safe, as login only works with password and salt and this call site requires login
|
||||
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
|
||||
let hash = user.password.as_ref().unwrap();
|
||||
let salt = user.salt.as_ref().unwrap();
|
||||
|
||||
if hash != &hash_plain_password_with_salt(current_password, salt)? {
|
||||
return Err(PasswordChangeError {
|
||||
message: "Aktuelles Passwort ist falsch.".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
if self.password != self.password_retyped {
|
||||
return Err(PasswordChangeError {
|
||||
message: "Passwörter stimmen nicht überein.".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// commits the password change to the database
|
||||
pub async fn commit(&self) -> Result<(), ApplicationError> {
|
||||
let (hash, salt) = generate_salt_and_hash_plain_password(self.password).unwrap();
|
||||
|
||||
User::update_password(self.pool, self.user_id, &hash, &salt).await?;
|
||||
|
||||
if let Some(token) = &self.token {
|
||||
token.delete(self.pool).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -167,7 +167,7 @@
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<p><b>Führungsassistent benötigt:</b> {% if event.voluntary_fuehrungsassistent %}ja{% else %}nein{% endif
|
||||
<p><b>Führungsassistent benötigt:</b> {% if event.fuehrungsassistent_required %}ja{% else %}nein{% endif
|
||||
%}</p>
|
||||
</div>
|
||||
|
||||
|
@ -12,8 +12,8 @@
|
||||
|
||||
<input type="hidden" name="date" value="{{ date }}">
|
||||
<input type="hidden" name="voluntarywachhabender" id="voluntarywachhabender" value="{{ voluntary_wachhabender }}">
|
||||
<input type="hidden" name="voluntaryfuehrungsassistent" id="voluntaryfuehrungsassistent"
|
||||
value="{{ voluntary_fuehrungsassistent }}">
|
||||
<input type="hidden" name="fuehrungsassistentrequired" id="fuehrungsassistentrequired"
|
||||
value="{{ fuehrungsassistent_required }}">
|
||||
|
||||
{% if let Some(id) = id %}
|
||||
<div class="field is-horizontal">
|
||||
@ -141,8 +141,8 @@
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input class="checkbox" type="checkbox" {{ (fa_disabled || canceled)|cond_show("disabled") }}
|
||||
{{ voluntary_fuehrungsassistent|cond_show("checked") }}
|
||||
_="on click put (the value of #voluntaryfuehrungsassistent) as inverseBool into the value of #voluntaryfuehrungsassistent">
|
||||
{{ fuehrungsassistent_required|cond_show("checked") }}
|
||||
_="on click put (the value of #fuehrungsassistentrequired) as inverseBool into the value of #fuehrungsassistentrequired">
|
||||
</label>
|
||||
</div>
|
||||
{% if fa_disabled %}
|
||||
|
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<p><b>Führungsassistent benötigt:</b> {% if event.voluntary_fuehrungsassistent %}ja{% else %}nein{% endif %}
|
||||
<p><b>Führungsassistent benötigt:</b> {% if event.fuehrungsassistent_required %}ja{% else %}nein{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user