Compare commits
10 Commits
f25e508bbd
...
45cf6dda10
Author | SHA1 | Date | |
---|---|---|---|
45cf6dda10 | |||
428f46b853 | |||
bdaf8ff20e | |||
512b061c7a | |||
2abeeb20df | |||
10e6ba80a2 | |||
e5df98a515 | |||
93574c3ac5 | |||
9893c37f80 | |||
f35b343768 |
@ -16,3 +16,4 @@ SMTP_PORT="1025"
|
|||||||
# SMTP_LOGIN=""
|
# SMTP_LOGIN=""
|
||||||
# SMTP_PASSWORD=""
|
# SMTP_PASSWORD=""
|
||||||
SMTP_TLSTYPE="none"
|
SMTP_TLSTYPE="none"
|
||||||
|
RUST_LOG="info,brass_web=trace,brass_db=trace"
|
||||||
|
70
Cargo.lock
generated
70
Cargo.lock
generated
@ -775,6 +775,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-std",
|
"async-std",
|
||||||
"brass-config",
|
"brass-config",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
]
|
]
|
||||||
@ -787,6 +788,19 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brass-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"fake",
|
||||||
|
"rand 0.9.1",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"sqlx",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brass-macros"
|
name = "brass-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -809,13 +823,13 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"askama",
|
"askama",
|
||||||
"brass-config",
|
"brass-config",
|
||||||
|
"brass-db",
|
||||||
"brass-macros",
|
"brass-macros",
|
||||||
"built",
|
"built",
|
||||||
"change-detection",
|
"change-detection",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fake",
|
"fake",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"garde",
|
|
||||||
"insta",
|
"insta",
|
||||||
"lettre",
|
"lettre",
|
||||||
"maud",
|
"maud",
|
||||||
@ -890,15 +904,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "castaway"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.22"
|
version = "1.2.22"
|
||||||
@ -1007,20 +1012,6 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "compact_str"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
|
||||||
dependencies = [
|
|
||||||
"castaway",
|
|
||||||
"cfg-if",
|
|
||||||
"itoa",
|
|
||||||
"rustversion",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -1643,31 +1634,6 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "garde"
|
|
||||||
version = "0.22.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f"
|
|
||||||
dependencies = [
|
|
||||||
"compact_str",
|
|
||||||
"garde_derive",
|
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "garde_derive"
|
|
||||||
version = "0.22.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -3383,12 +3349,6 @@ dependencies = [
|
|||||||
"path-slash",
|
"path-slash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [ "cli", "config", "macros", "web", ]
|
members = [ "cli", "config", "db", "macros", "web", ]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
default-members = ["web"]
|
default-members = ["web"]
|
||||||
|
|
||||||
|
@ -15,3 +15,4 @@ brass-config = { path = "../config" }
|
|||||||
async-std = { version = "1.13.0", features = ["attributes"] }
|
async-std = { version = "1.13.0", features = ["attributes"] }
|
||||||
sqlx = { version = "0.8.2", features = ["runtime-async-std", "postgres"] }
|
sqlx = { version = "0.8.2", features = ["runtime-async-std", "postgres"] }
|
||||||
anyhow = "1.0.94"
|
anyhow = "1.0.94"
|
||||||
|
chrono = "0.4.41"
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use chrono::Local;
|
||||||
use sqlx::migrate::Migrate;
|
use sqlx::migrate::Migrate;
|
||||||
use sqlx::{migrate::Migrator, Executor};
|
use sqlx::{migrate::Migrator, Executor};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -28,9 +31,14 @@ enum Command {
|
|||||||
Reset,
|
Reset,
|
||||||
#[command(about = "Run all pending migrations on database")]
|
#[command(about = "Run all pending migrations on database")]
|
||||||
Migrate,
|
Migrate,
|
||||||
|
#[command(about = "Create a new migration")]
|
||||||
|
NewMigration { title: String },
|
||||||
|
#[command(about = "Prepare sqlx query metadata for offline compile-time verification")]
|
||||||
|
Prepare,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
|
#[allow(unused)]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = load_config(&cli.environment).expect("Could not load config!");
|
let config = load_config(&cli.environment).expect("Could not load config!");
|
||||||
@ -42,7 +50,6 @@ async fn main() {
|
|||||||
create_db(&db_config)
|
create_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed creating database.");
|
.expect("Failed creating database.");
|
||||||
|
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.expect("Failed migrating database.");
|
||||||
@ -51,20 +58,24 @@ async fn main() {
|
|||||||
drop_db(&db_config)
|
drop_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed dropping database.");
|
.expect("Failed dropping database.");
|
||||||
|
|
||||||
create_db(&db_config)
|
create_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed creating database.");
|
.expect("Failed creating database.");
|
||||||
|
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.expect("Failed migrating database.");
|
||||||
},
|
}
|
||||||
Command::Migrate => {
|
Command::Migrate => {
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.expect("Failed migrating database.");
|
||||||
}
|
}
|
||||||
|
Command::NewMigration { title } => {
|
||||||
|
create_new_migration(&title)
|
||||||
|
.await
|
||||||
|
.expect("Failed creating new migration.");
|
||||||
|
}
|
||||||
|
Command::Prepare => prepare().await.expect("Failed preparing query metadata."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,13 +122,7 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Connection to database failed!")?;
|
.context("Connection to database failed!")?;
|
||||||
|
|
||||||
let migrations_path = PathBuf::from(
|
let migrations_path = db_package_root()?.join("migrations");
|
||||||
std::env::var("CARGO_MANIFEST_DIR").expect("This command needs to be invoked using cargo"),
|
|
||||||
)
|
|
||||||
.join("..")
|
|
||||||
.join("migrations")
|
|
||||||
.canonicalize()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let migrator = Migrator::new(Path::new(&migrations_path))
|
let migrator = Migrator::new(Path::new(&migrations_path))
|
||||||
.await
|
.await
|
||||||
@ -148,3 +153,56 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_new_migration(title: &str) -> anyhow::Result<()> {
|
||||||
|
let now = Local::now();
|
||||||
|
let timestamp = now.format("%Y%m%d%H%M%S");
|
||||||
|
let file_name = format!("{timestamp}_{title}.sql");
|
||||||
|
let path = db_package_root()?.join("migrations").join(&file_name);
|
||||||
|
|
||||||
|
let mut file = File::create(&path).context(format!(r#"Could not create file "{:?}""#, path))?;
|
||||||
|
file.write_all("".as_bytes())
|
||||||
|
.context(format!(r#"Could not write file "{:?}""#, path))?;
|
||||||
|
|
||||||
|
println!("Created migration {file_name}.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare() -> anyhow::Result<()> {
|
||||||
|
let cargo = std::env::var("CARGO")
|
||||||
|
.map_err(|_| anyhow::anyhow!("Please invoke me using Cargo, e.g.: `cargo db <ARGS>`"))
|
||||||
|
.expect("Existence of CARGO env var is asserted by calling `ensure_sqlx_cli_installed`");
|
||||||
|
|
||||||
|
let mut sqlx_prepare_command = {
|
||||||
|
let mut cmd = std::process::Command::new(&cargo);
|
||||||
|
|
||||||
|
cmd.args(["sqlx", "prepare", "--", "--all-targets", "--all-features"]);
|
||||||
|
|
||||||
|
let cmd_cwd = db_package_root().context("Error finding the root of the db package!")?;
|
||||||
|
cmd.current_dir(cmd_cwd);
|
||||||
|
|
||||||
|
cmd
|
||||||
|
};
|
||||||
|
|
||||||
|
let o = sqlx_prepare_command
|
||||||
|
.output()
|
||||||
|
.context("Could not run {cargo} sqlx prepare!")?;
|
||||||
|
|
||||||
|
if !o.status.success() {
|
||||||
|
let error = anyhow::anyhow!(String::from_utf8_lossy(&o.stdout).to_string()).context("Error generating query metadata. Are you sure the database is running and all migrations are applied?");
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Query data written to db/.sqlx directory; please check this into version control.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_package_root() -> Result<PathBuf, anyhow::Error> {
|
||||||
|
Ok(PathBuf::from(
|
||||||
|
std::env::var("CARGO_MANIFEST_DIR").expect("This command needs to be invoked using cargo"),
|
||||||
|
)
|
||||||
|
.join("..")
|
||||||
|
.join("db")
|
||||||
|
.canonicalize()?)
|
||||||
|
}
|
||||||
|
19
db/Cargo.toml
Normal file
19
db/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "brass-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0"
|
||||||
|
authors = ["Max Hohlfeld <maxhohlfeld@posteo.de>"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sqlx = { version = "^0.8", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
|
||||||
|
chrono = { version = "0.4.33", features = ["serde", "now"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
rand = { version = "0.9", features = ["os_rng"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
fake = { version = "4", features = ["chrono", "derive"], optional = true}
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-helpers = ["dep:fake"]
|
31
db/src/lib.rs
Normal file
31
db/src/lib.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
pub mod models;
|
||||||
|
mod support;
|
||||||
|
pub mod validation;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use chrono::NaiveTime;
|
||||||
|
|
||||||
|
pub use support::{NoneToken, Token};
|
||||||
|
|
||||||
|
const START_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||||
|
const END_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(23, 59, 59).unwrap();
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnsupportedEnumValue {
|
||||||
|
pub value: u8,
|
||||||
|
pub enum_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for UnsupportedEnumValue {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"unsupported enum value '{}' given for enum '{}'",
|
||||||
|
self.value, self.enum_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for UnsupportedEnumValue {}
|
@ -1,8 +1,9 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{PgPool, query};
|
||||||
|
|
||||||
use super::{assignment_changeset::AssignmentChangeset, Function, Result};
|
use super::{AssignmentChangeset, Function, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Assignment {
|
pub struct Assignment {
|
||||||
pub event_id: i32,
|
pub event_id: i32,
|
||||||
pub availability_id: i32,
|
pub availability_id: i32,
|
186
db/src/models/assignment_changeset.rs
Normal file
186
db/src/models/assignment_changeset.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::validation::{
|
||||||
|
AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Assignment, Availability, Event, Function, Role, User};
|
||||||
|
|
||||||
|
pub struct AssignmentChangeset {
|
||||||
|
pub function: Function,
|
||||||
|
pub time: (NaiveDateTime, NaiveDateTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssignmentContext<'a> {
|
||||||
|
pub pool: &'a PgPool,
|
||||||
|
pub user: &'a User,
|
||||||
|
pub event_id: i32,
|
||||||
|
pub availability_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsyncValidate<'a> for AssignmentChangeset {
|
||||||
|
type Context = AssignmentContext<'a>;
|
||||||
|
|
||||||
|
async fn validate_with_context(
|
||||||
|
&self,
|
||||||
|
context: &'a Self::Context,
|
||||||
|
) -> Result<(), crate::validation::AsyncValidateError> {
|
||||||
|
let Some(availability) =
|
||||||
|
Availability::read_by_id_including_user(context.pool, context.availability_id).await?
|
||||||
|
else {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Angegebener Verfügbarkeit des Nutzers existiert nicht.",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(event) =
|
||||||
|
Event::read_by_id_including_location(context.pool, context.event_id).await?
|
||||||
|
else {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Angegebene Veranstaltung existiert nicht.",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
user_is_admin_or_area_manager_of_event_area(context.user, &event)?;
|
||||||
|
availability_user_inside_event_area(&availability, &event)?;
|
||||||
|
available_time_fits(&self.time, &availability)?;
|
||||||
|
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||||
|
availability_not_already_assigned(&self.time, &availability, &event, context.pool).await?;
|
||||||
|
user_of_availability_has_function(&self.function, &availability)?;
|
||||||
|
event_has_free_slot_for_function(&self.function, &availability, &event, context.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn availability_user_inside_event_area(
|
||||||
|
availability: &Availability,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let user = availability.user.as_ref().unwrap();
|
||||||
|
let location = event.location.as_ref().unwrap();
|
||||||
|
|
||||||
|
if user.area_id != location.area_id {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Nutzer der Verfügbarkeit ist nicht im gleichen Bereich wie der Ort der Veranstaltung.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_time_fits(
|
||||||
|
value: &(NaiveDateTime, NaiveDateTime),
|
||||||
|
availability: &Availability,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if value.0 < availability.start || value.1 > availability.end {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Die verfügbar gemachte Zeit passt nicht zu der zugewiesenen Zeit für die Veranstaltung.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_of_availability_has_function(
|
||||||
|
value: &Function,
|
||||||
|
availability: &Availability,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let user_function = &availability.user.as_ref().unwrap().function;
|
||||||
|
|
||||||
|
if !user_function.contains(value) {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Nutzer der Verfügbarkeit besitzt nicht die benötigte Funktion um für diese Position zugewiesen zu werden.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn event_has_free_slot_for_function(
|
||||||
|
value: &Function,
|
||||||
|
availability: &Availability,
|
||||||
|
event: &Event,
|
||||||
|
pool: &PgPool,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
debug!(?event, "event parameter");
|
||||||
|
let assignments_for_event: Vec<Assignment> = Assignment::read_all_by_event(pool, event.id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.availability_id != availability.id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug!(?assignments_for_event, "existing assignments for event");
|
||||||
|
|
||||||
|
let assignments_with_function = assignments_for_event
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.function == *value)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
assignments_with_function,
|
||||||
|
"amount of existing assignments 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
|
||||||
|
}
|
||||||
|
Function::Wachhabender => event.voluntary_wachhabender && assignments_with_function >= 1,
|
||||||
|
} {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Veranstaltung hat bereits genug Zuweisungen für diese Funktion.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn availability_not_already_assigned(
|
||||||
|
time: &(NaiveDateTime, NaiveDateTime),
|
||||||
|
availability: &Availability,
|
||||||
|
event: &Event,
|
||||||
|
pool: &PgPool,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let list: Vec<Assignment> = Assignment::read_all_by_availability(pool, availability.id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.event_id != event.id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let has_start_time_during_assignment = |a: &Assignment| a.start >= time.0 && a.start <= time.1;
|
||||||
|
let has_end_time_during_assignment = |a: &Assignment| a.end >= time.0 && a.end <= time.1;
|
||||||
|
|
||||||
|
if list
|
||||||
|
.iter()
|
||||||
|
.any(|a| has_start_time_during_assignment(a) || has_end_time_during_assignment(a))
|
||||||
|
{
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Die Verfügbarkeit des Nutzers wurde bereits zu einer anderen Veranstaltung zugewiesen.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe merge with event changeset
|
||||||
|
fn user_is_admin_or_area_manager_of_event_area(
|
||||||
|
user: &User,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let user_is_admin = user.role == Role::Admin;
|
||||||
|
let user_is_area_manager_event_area =
|
||||||
|
user.role == Role::AreaManager && user.area_id == event.location.as_ref().unwrap().area_id;
|
||||||
|
|
||||||
|
if !user_is_admin && !user_is_area_manager_event_area {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Du verfügst nicht über die Berechtigung, Zuweisungen zu Veranstaltungen vorzunehmen.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{PgPool, query};
|
||||||
|
|
||||||
use super::{Area, AvailabilityChangeset, Result, Role, User, UserFunction};
|
use super::{Area, AvailabilityChangeset, Result, Role, User, UserFunction};
|
||||||
|
|
@ -1,14 +1,10 @@
|
|||||||
use chrono::{Days, NaiveDateTime};
|
use chrono::{Days, NaiveDateTime};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
utils::validation::{
|
|
||||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError,
|
|
||||||
},
|
|
||||||
END_OF_DAY, START_OF_DAY,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Availability;
|
use super::Availability;
|
||||||
|
use crate::{validation::{
|
||||||
|
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError
|
||||||
|
}, END_OF_DAY, START_OF_DAY};
|
||||||
|
|
||||||
pub struct AvailabilityChangeset {
|
pub struct AvailabilityChangeset {
|
||||||
pub time: (NaiveDateTime, NaiveDateTime),
|
pub time: (NaiveDateTime, NaiveDateTime),
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{PgPool, query};
|
||||||
|
|
||||||
use super::{event_changeset::EventChangeset, Clothing, Location, Result};
|
use super::{Clothing, EventChangeset, Location, Result};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Event {
|
pub struct Event {
|
248
db/src/models/event_changeset.rs
Normal file
248
db/src/models/event_changeset.rs
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
use chrono::Days;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
#[cfg(feature = "test-helpers")]
|
||||||
|
use fake::{Fake, Faker};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::END_OF_DAY;
|
||||||
|
use crate::START_OF_DAY;
|
||||||
|
use crate::models::Assignment;
|
||||||
|
use crate::models::Availability;
|
||||||
|
use crate::models::Event;
|
||||||
|
use crate::models::Function;
|
||||||
|
use crate::models::Location;
|
||||||
|
use crate::models::Role;
|
||||||
|
use crate::models::User;
|
||||||
|
use crate::validation::AsyncValidate;
|
||||||
|
use crate::validation::AsyncValidateError;
|
||||||
|
use crate::validation::start_date_time_lies_before_end_date_time;
|
||||||
|
|
||||||
|
pub struct EventChangeset {
|
||||||
|
pub time: (NaiveDateTime, NaiveDateTime),
|
||||||
|
pub name: String,
|
||||||
|
pub location_id: i32,
|
||||||
|
pub voluntary_wachhabender: bool,
|
||||||
|
pub voluntary_fuehrungsassistent: bool,
|
||||||
|
pub amount_of_posten: i16,
|
||||||
|
pub clothing: i32,
|
||||||
|
pub note: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventContext<'a> {
|
||||||
|
pub pool: &'a PgPool,
|
||||||
|
pub event: Option<i32>,
|
||||||
|
pub user: &'a User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsyncValidate<'a> for EventChangeset {
|
||||||
|
type Context = EventContext<'a>;
|
||||||
|
|
||||||
|
async fn validate_with_context(
|
||||||
|
&self,
|
||||||
|
context: &'a Self::Context,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let Some(location) = Location::read_by_id(context.pool, self.location_id).await? else {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Der angegebene Veranstaltungsort existiert nicht.",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
user_is_admin_or_area_manager_of_event_location(context.user, &location)?;
|
||||||
|
|
||||||
|
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||||
|
|
||||||
|
let mut minimum_amount_of_posten = 0_i16;
|
||||||
|
if let Some(id) = context.event {
|
||||||
|
let event = Event::read_by_id_including_location(context.pool, id)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
let assignments_for_event =
|
||||||
|
Assignment::read_all_by_event(context.pool, event.id).await?;
|
||||||
|
minimum_amount_of_posten = assignments_for_event
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.function == Function::Posten)
|
||||||
|
.count() as i16;
|
||||||
|
|
||||||
|
time_can_be_extended_if_edit(&self.time, &event, &assignments_for_event, context.pool)
|
||||||
|
.await?;
|
||||||
|
date_unchanged_if_edit(&self.time, &event.start.date())?;
|
||||||
|
can_unset_wachhabender(&self.voluntary_wachhabender, &assignments_for_event)?;
|
||||||
|
can_unset_fuehrungsassistent(
|
||||||
|
&self.voluntary_fuehrungsassistent,
|
||||||
|
&assignments_for_event,
|
||||||
|
)?;
|
||||||
|
if location.area_id != event.location.unwrap().area_id {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Veranstaltungsort kann nicht zu einem Ort außerhalb des initialen Bereichs geändert werden.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(minimum_amount_of_posten..=100).contains(&self.amount_of_posten) {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Die Anzahl der Posten darf nicht kleiner als die Anzahl der bereits geplanten Posten und maximal 100 sein.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_is_admin_or_area_manager_of_event_location(
|
||||||
|
user: &User,
|
||||||
|
location: &Location,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if user.role != Role::Admin
|
||||||
|
&& !(user.role == Role::AreaManager && user.area_id == location.area_id)
|
||||||
|
{
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"Du verfügst nicht über die Berechtigung, diese Veranstaltung zu erstellen bzw. zu bearbeiten.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date_unchanged_if_edit(
|
||||||
|
time: &(NaiveDateTime, NaiveDateTime),
|
||||||
|
date_in_db: &NaiveDate,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if time.0.date() != *date_in_db {
|
||||||
|
return Err(AsyncValidateError::new("event date can't be changed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn time_can_be_extended_if_edit(
|
||||||
|
time: &(NaiveDateTime, NaiveDateTime),
|
||||||
|
event: &Event,
|
||||||
|
assignments_for_event: &Vec<Assignment>,
|
||||||
|
pool: &PgPool,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let start = event.start.date();
|
||||||
|
let end = event.start.date().checked_add_days(Days::new(1)).unwrap();
|
||||||
|
let mut common_time = (start.and_time(START_OF_DAY), end.and_time(END_OF_DAY));
|
||||||
|
|
||||||
|
for assignment in assignments_for_event {
|
||||||
|
let availability = Availability::read_by_id(pool, assignment.availability_id)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
let all_assignments =
|
||||||
|
Assignment::read_all_by_availability(pool, assignment.availability_id).await?;
|
||||||
|
|
||||||
|
if all_assignments.len() == 1 {
|
||||||
|
if availability.start > common_time.0 {
|
||||||
|
common_time.0 = availability.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if availability.end < common_time.1 {
|
||||||
|
common_time.1 = availability.end;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut slots = vec![(availability.start, availability.end)];
|
||||||
|
for a in all_assignments
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.event_id != assignment.event_id)
|
||||||
|
{
|
||||||
|
let (fit, rest) = slots
|
||||||
|
.into_iter()
|
||||||
|
.partition(|s| s.0 >= a.start && s.1 <= a.end);
|
||||||
|
slots = rest;
|
||||||
|
let fit = fit.first().unwrap();
|
||||||
|
|
||||||
|
if fit.0 != a.start {
|
||||||
|
slots.push((fit.0, a.start));
|
||||||
|
}
|
||||||
|
|
||||||
|
if fit.1 != a.end {
|
||||||
|
slots.push((a.end, fit.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = slots
|
||||||
|
.into_iter()
|
||||||
|
.find(|s| s.0 >= assignment.start && s.1 <= assignment.end)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if slot.0 > common_time.0 {
|
||||||
|
common_time.0 = slot.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if slot.1 < common_time.1 {
|
||||||
|
common_time.1 = slot.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_start_time = common_time.0;
|
||||||
|
let new_start_time = time.0;
|
||||||
|
let old_end_time = common_time.1;
|
||||||
|
let new_end_time = time.1;
|
||||||
|
|
||||||
|
if new_start_time < old_start_time {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"starttime lies outside of available time for assigned people",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_end_time > old_end_time {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"endtime lies ouside of available time for assigned people",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_unset_fuehrungsassistent(
|
||||||
|
fuehrungsassistent_required: &bool,
|
||||||
|
assignments_for_event: &Vec<Assignment>,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if !*fuehrungsassistent_required
|
||||||
|
&& assignments_for_event
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.function == Function::Fuehrungsassistent)
|
||||||
|
{
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"fuehrungsassistent can't be set to not by ff, because a person is already assigned",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_unset_wachhabender(
|
||||||
|
voluntary_wachhabender: &bool,
|
||||||
|
assignments_for_event: &Vec<Assignment>,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if !*voluntary_wachhabender
|
||||||
|
&& assignments_for_event
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.function == Function::Wachhabender)
|
||||||
|
{
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"wachhabender can't be set to not by ff, because a person is already assigned",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-helpers")]
|
||||||
|
impl EventChangeset {
|
||||||
|
pub fn create_for_test(start: NaiveDateTime, end: NaiveDateTime) -> EventChangeset {
|
||||||
|
let changeset = EventChangeset {
|
||||||
|
time: (start, end),
|
||||||
|
name: Faker.fake(),
|
||||||
|
location_id: 1,
|
||||||
|
voluntary_wachhabender: true,
|
||||||
|
voluntary_fuehrungsassistent: true,
|
||||||
|
amount_of_posten: 5,
|
||||||
|
clothing: 1,
|
||||||
|
note: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
changeset
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,11 @@
|
|||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
PgPool,
|
||||||
postgres::{PgHasArrayType, PgTypeInfo},
|
postgres::{PgHasArrayType, PgTypeInfo},
|
||||||
query, PgPool,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::ApplicationError;
|
use super::{Function, Result};
|
||||||
|
|
||||||
use super::Function;
|
|
||||||
|
|
||||||
pub struct ExportEventRow {
|
pub struct ExportEventRow {
|
||||||
pub start_timestamp: NaiveDateTime,
|
pub start_timestamp: NaiveDateTime,
|
||||||
@ -38,7 +37,7 @@ impl ExportEventRow {
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
time: (NaiveDate, NaiveDate),
|
time: (NaiveDate, NaiveDate),
|
||||||
area: i32,
|
area: i32,
|
||||||
) -> Result<Vec<ExportEventRow>, ApplicationError> {
|
) -> Result<Vec<ExportEventRow>> {
|
||||||
let rows = query!(
|
let rows = query!(
|
||||||
"select
|
"select
|
||||||
event.starttimestamp,
|
event.starttimestamp,
|
@ -1,8 +1,9 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::utils::ApplicationError;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::UnsupportedEnumValue;
|
||||||
|
|
||||||
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, PartialOrd, Ord)]
|
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, PartialOrd, Ord)]
|
||||||
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
||||||
pub enum Function {
|
pub enum Function {
|
||||||
@ -22,16 +23,16 @@ impl Display for Function {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Function {
|
impl TryFrom<u8> for Function {
|
||||||
type Error = ApplicationError;
|
type Error = UnsupportedEnumValue;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
1 => Ok(Function::Posten),
|
1 => Ok(Function::Posten),
|
||||||
5 => Ok(Function::Fuehrungsassistent),
|
5 => Ok(Function::Fuehrungsassistent),
|
||||||
10 => Ok(Function::Wachhabender),
|
10 => Ok(Function::Wachhabender),
|
||||||
_ => Err(ApplicationError::UnsupportedEnumValue {
|
_ => Err(UnsupportedEnumValue {
|
||||||
value: value.to_string(),
|
value,
|
||||||
enum_name: String::from("Function"),
|
enum_name: "Function",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,6 @@
|
|||||||
use sqlx::{query, PgPool};
|
use sqlx::{PgPool, query};
|
||||||
|
|
||||||
use super::Area;
|
use super::{Area, Result};
|
||||||
|
|
||||||
use super::Result;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Location {
|
pub struct Location {
|
@ -25,7 +25,7 @@ pub use assignment_changeset::{AssignmentChangeset, AssignmentContext};
|
|||||||
pub use availability::Availability;
|
pub use availability::Availability;
|
||||||
pub use availability_assignment_state::AvailabilityAssignmentState;
|
pub use availability_assignment_state::AvailabilityAssignmentState;
|
||||||
pub use availability_changeset::{
|
pub use availability_changeset::{
|
||||||
find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext,
|
AvailabilityChangeset, AvailabilityContext, find_free_date_time_slots,
|
||||||
};
|
};
|
||||||
pub use clothing::Clothing;
|
pub use clothing::Clothing;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
@ -33,7 +33,7 @@ pub use event_changeset::{EventChangeset, EventContext};
|
|||||||
pub use export_event_row::{ExportEventRow, SimpleAssignment};
|
pub use export_event_row::{ExportEventRow, SimpleAssignment};
|
||||||
pub use function::Function;
|
pub use function::Function;
|
||||||
pub use location::Location;
|
pub use location::Location;
|
||||||
pub use password_reset::{NoneToken, PasswordReset, Token};
|
pub use password_reset::PasswordReset;
|
||||||
pub use registration::Registration;
|
pub use registration::Registration;
|
||||||
pub use role::Role;
|
pub use role::Role;
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
@ -42,17 +42,4 @@ pub use user_funtion::UserFunction;
|
|||||||
pub use vehicle::Vehicle;
|
pub use vehicle::Vehicle;
|
||||||
pub use vehicle_assignment::VehicleAssignment;
|
pub use vehicle_assignment::VehicleAssignment;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, sqlx::Error>;
|
type Result<T> = std::result::Result<T, sqlx::Error>;
|
||||||
|
|
||||||
fn start_date_time_lies_before_end_date_time<T>(
|
|
||||||
value: &(NaiveDateTime, NaiveDateTime),
|
|
||||||
_context: &T,
|
|
||||||
) -> garde::Result {
|
|
||||||
if value.0 >= value.1 {
|
|
||||||
return Err(garde::Error::new("endtime can't lie before starttime"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,19 +1,9 @@
|
|||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use sqlx::{query_as, PgPool};
|
use sqlx::{PgPool, query_as};
|
||||||
|
|
||||||
|
use crate::support::{Token, generate_token_and_expiration};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::utils::token_generation::generate_token_and_expiration;
|
|
||||||
|
|
||||||
pub trait Token {
|
|
||||||
async fn delete(&self, pool: &PgPool) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NoneToken {}
|
|
||||||
impl Token for NoneToken {
|
|
||||||
async fn delete(&self, _pool: &PgPool) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PasswordReset {
|
pub struct PasswordReset {
|
@ -1,7 +1,5 @@
|
|||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use sqlx::{query_as, PgPool};
|
use sqlx::{PgPool, query_as};
|
||||||
|
|
||||||
use crate::utils::token_generation::generate_token_and_expiration;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Registration {
|
pub struct Registration {
|
||||||
@ -9,7 +7,9 @@ pub struct Registration {
|
|||||||
pub userid: i32,
|
pub userid: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::{password_reset::Token, Result};
|
use crate::support::{Token, generate_token_and_expiration};
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
|
||||||
impl Registration {
|
impl Registration {
|
||||||
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<Registration> {
|
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<Registration> {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user