forked from Ruderverein-Donau-Linz/rowt
1107 lines
31 KiB
Rust
1107 lines
31 KiB
Rust
use std::ops::{Deref, DerefMut};
|
|
|
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
|
use chrono::{Datelike, Local, NaiveDate};
|
|
use log::info;
|
|
use rocket::{
|
|
async_trait,
|
|
http::{Cookie, Status},
|
|
request::{self, FromRequest, Outcome},
|
|
time::{Duration, OffsetDateTime},
|
|
Request,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
|
|
|
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
|
|
use crate::tera::admin::user::UserEditForm;
|
|
|
|
const RENNRUDERBEITRAG: i32 = 11000;
|
|
const BOAT_STORAGE: i32 = 4500;
|
|
const FAMILY_TWO: i32 = 30000;
|
|
const FAMILY_THREE_OR_MORE: i32 = 35000;
|
|
const STUDENT_OR_PUPIL: i32 = 8000;
|
|
const REGULAR: i32 = 22000;
|
|
const UNTERSTUETZEND: i32 = 2500;
|
|
const FOERDERND: i32 = 8500;
|
|
|
|
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
|
pub struct User {
|
|
pub id: i64,
|
|
pub name: String,
|
|
pub pw: Option<String>,
|
|
pub dob: Option<String>,
|
|
pub weight: Option<String>,
|
|
pub sex: Option<String>,
|
|
pub deleted: bool,
|
|
pub last_access: Option<chrono::NaiveDateTime>,
|
|
pub member_since_date: Option<String>,
|
|
pub birthdate: Option<String>,
|
|
pub mail: Option<String>,
|
|
pub nickname: Option<String>,
|
|
pub notes: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub address: Option<String>,
|
|
pub family_id: Option<i64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct UserWithRoles {
|
|
#[serde(flatten)]
|
|
pub user: User,
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
impl UserWithRoles {
|
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
|
Self {
|
|
roles: user.roles(db).await,
|
|
user,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct UserWithWaterStatus {
|
|
#[serde(flatten)]
|
|
pub user: User,
|
|
pub on_water: bool,
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
impl UserWithWaterStatus {
|
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
|
Self {
|
|
on_water: user.on_water(db).await,
|
|
roles: user.roles(db).await,
|
|
user,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialEq for User {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.id == other.id
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum LoginError {
|
|
InvalidAuthenticationCombo,
|
|
UserNotFound,
|
|
UserDeleted,
|
|
NotLoggedIn,
|
|
NotAnAdmin,
|
|
NotACox,
|
|
NotATech,
|
|
GuestNotAllowed,
|
|
NoPasswordSet(User),
|
|
DeserializationError,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct Fee {
|
|
pub sum_in_cents: i32,
|
|
pub parts: Vec<(String, i32)>,
|
|
pub name: String,
|
|
pub user_ids: String,
|
|
pub paid: bool,
|
|
pub users: Vec<User>,
|
|
}
|
|
|
|
impl Default for Fee {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Fee {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sum_in_cents: 0,
|
|
name: "".into(),
|
|
parts: Vec::new(),
|
|
user_ids: "".into(),
|
|
users: Vec::new(),
|
|
paid: false,
|
|
}
|
|
}
|
|
|
|
pub fn add(&mut self, desc: String, price_in_cents: i32) {
|
|
self.sum_in_cents += price_in_cents;
|
|
|
|
self.parts.push((desc, price_in_cents));
|
|
}
|
|
|
|
pub fn add_person(&mut self, user: &User) {
|
|
if !self.name.is_empty() {
|
|
self.name.push_str(" + ");
|
|
self.user_ids.push('&');
|
|
}
|
|
self.name.push_str(&user.name);
|
|
|
|
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
|
|
self.users.push(user.clone());
|
|
}
|
|
|
|
pub fn paid(&mut self) {
|
|
self.paid = true;
|
|
}
|
|
|
|
pub fn merge(&mut self, fee: Fee) {
|
|
for (desc, price_in_cents) in fee.parts {
|
|
self.add(desc, price_in_cents);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl User {
|
|
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
|
if !self.has_role(db, "Donau Linz").await {
|
|
return None;
|
|
}
|
|
if self.deleted {
|
|
return None;
|
|
}
|
|
|
|
let mut fee = Fee::new();
|
|
|
|
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
|
for member in family.members(db).await {
|
|
fee.add_person(&member);
|
|
if member.has_role(db, "paid").await {
|
|
fee.paid();
|
|
}
|
|
fee.merge(member.fee_without_families(db).await);
|
|
}
|
|
if family.amount_family_members(db).await > 2 {
|
|
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
|
|
} else {
|
|
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
|
|
}
|
|
} else {
|
|
fee.add_person(self);
|
|
if self.has_role(db, "paid").await {
|
|
fee.paid();
|
|
}
|
|
fee.merge(self.fee_without_families(db).await);
|
|
}
|
|
|
|
Some(fee)
|
|
}
|
|
|
|
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
|
|
let mut fee = Fee::new();
|
|
|
|
if !self.has_role(db, "Donau Linz").await {
|
|
return fee;
|
|
}
|
|
if self.has_role(db, "Rennrudern").await {
|
|
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
|
|
}
|
|
|
|
let amount_boats = self.amount_boats(db).await;
|
|
if amount_boats > 0 {
|
|
fee.add(
|
|
format!("{}x Bootsplatz", amount_boats),
|
|
amount_boats * BOAT_STORAGE,
|
|
);
|
|
}
|
|
|
|
if self.has_role(db, "Unterstützend").await {
|
|
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
|
|
} else if self.has_role(db, "Förderndes Mitglied").await {
|
|
fee.add("Förderndes Mitglied".into(), FOERDERND);
|
|
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
|
|
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
|
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
|
} else if self.has_role(db, "Ehrenmitglied").await {
|
|
fee.add("Ehrenmitglied".into(), 0);
|
|
} else {
|
|
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
|
}
|
|
}
|
|
|
|
fee
|
|
}
|
|
|
|
pub async fn amount_boats(&self, db: &SqlitePool) -> i32 {
|
|
sqlx::query!(
|
|
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
|
|
self.id
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.unwrap()
|
|
.count
|
|
}
|
|
|
|
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
|
if sqlx::query!(
|
|
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
|
self.id,
|
|
role
|
|
)
|
|
.fetch_optional(db)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
|
sqlx::query!(
|
|
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
|
self.id
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
.into_iter().map(|r| r.name).collect()
|
|
}
|
|
|
|
pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
|
|
if sqlx::query!(
|
|
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
|
self.id,
|
|
role
|
|
)
|
|
.fetch_optional(db.deref_mut())
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE id like ?
|
|
",
|
|
id
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE id like ?
|
|
",
|
|
id
|
|
)
|
|
.fetch_one(db.deref_mut())
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE name like ?
|
|
",
|
|
name
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
|
if sqlx::query!(
|
|
"SELECT * FROM logbook WHERE shipmaster=? AND arrival is null",
|
|
self.id
|
|
)
|
|
.fetch_optional(db)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
if sqlx::query!(
|
|
"SELECT * FROM logbook JOIN rower ON rower.logbook_id=logbook.id WHERE rower_id=? AND arrival is null",
|
|
self.id
|
|
)
|
|
.fetch_optional(db)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE deleted = 0
|
|
ORDER BY last_access DESC
|
|
"
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user u
|
|
JOIN user_role ur ON u.id = ur.user_id
|
|
WHERE ur.role_id = ? AND deleted = 0
|
|
ORDER BY name;
|
|
", role.id
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn all_payer_groups(db: &SqlitePool) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
|
|
WHERE family_id IS NOT NULL
|
|
GROUP BY family_id
|
|
|
|
UNION
|
|
|
|
-- Select users with a null family_id, without grouping
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
|
|
WHERE family_id IS NULL;
|
|
"
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
|
ORDER BY name
|
|
"
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
|
FROM user
|
|
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
|
ORDER BY last_access DESC
|
|
"
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
|
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
|
.execute(db)
|
|
.await
|
|
.is_ok()
|
|
}
|
|
|
|
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) {
|
|
let mut family_id = data.family_id;
|
|
|
|
if family_id.is_some_and(|x| x == -1) {
|
|
family_id = Some(Family::insert(db).await)
|
|
}
|
|
|
|
sqlx::query!(
|
|
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
|
|
data.dob,
|
|
data.weight,
|
|
data.sex,
|
|
data.member_since_date,
|
|
data.birthdate,
|
|
data.mail,
|
|
data.nickname,
|
|
data.notes,
|
|
data.phone,
|
|
data.address,
|
|
family_id,
|
|
self.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
|
|
// handle roles
|
|
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
|
|
for role_id in data.roles.into_keys() {
|
|
self.add_role(
|
|
db,
|
|
&Role::find_by_id(db, role_id.parse::<i32>().unwrap())
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn add_role(&self, db: &SqlitePool, role: &Role) {
|
|
sqlx::query!(
|
|
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
|
self.id,
|
|
role.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
|
|
sqlx::query!(
|
|
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
|
self.id,
|
|
role.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
|
let name = name.trim(); // just to make sure...
|
|
let Some(user) = User::find_by_name(db, name).await else {
|
|
if ![
|
|
"n-sageder",
|
|
"p-hofer",
|
|
"m-birner",
|
|
"s-sollberger",
|
|
"d-kortschak",
|
|
"wwwadmin",
|
|
"wadminw",
|
|
"admin",
|
|
"m sageder",
|
|
"d kortschak",
|
|
"a almousa",
|
|
"p hofer",
|
|
"s sollberger",
|
|
"n sageder",
|
|
"wp-system",
|
|
"s.sollberger",
|
|
"m.birner",
|
|
"m-sageder",
|
|
"a-almousa",
|
|
"m.sageder",
|
|
"n.sageder",
|
|
"a.almousa",
|
|
"p.hofer",
|
|
"d.kortschak",
|
|
"[login]",
|
|
]
|
|
.contains(&name)
|
|
{
|
|
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
|
}
|
|
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
|
|
};
|
|
|
|
if user.deleted {
|
|
Log::create(
|
|
db,
|
|
format!("User ({name}) already deleted (tried to login)."),
|
|
)
|
|
.await;
|
|
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
|
|
//been deleted
|
|
}
|
|
|
|
if let Some(user_pw) = user.pw.as_ref() {
|
|
let password_hash = &Self::get_hashed_pw(pw);
|
|
if password_hash == user_pw {
|
|
return Ok(user);
|
|
}
|
|
Log::create(db, format!("User {name} supplied the wrong PW")).await;
|
|
Err(LoginError::InvalidAuthenticationCombo)
|
|
} else {
|
|
info!("User {name} has no PW set");
|
|
Err(LoginError::NoPasswordSet(user))
|
|
}
|
|
}
|
|
|
|
pub async fn reset_pw(&self, db: &SqlitePool) {
|
|
sqlx::query!("UPDATE user SET pw = null where id = ?", self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
|
|
let pw = Self::get_hashed_pw(pw);
|
|
sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
fn get_hashed_pw(pw: &str) -> String {
|
|
let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap();
|
|
let argon2 = Argon2::default();
|
|
argon2
|
|
.hash_password(pw.as_bytes(), &salt)
|
|
.unwrap()
|
|
.to_string()
|
|
}
|
|
|
|
pub async fn logged_in(&self, db: &SqlitePool) {
|
|
sqlx::query!(
|
|
"UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?",
|
|
self.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn delete(&self, db: &SqlitePool) {
|
|
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
|
let mut days = Vec::new();
|
|
for i in 0..self.amount_days_to_show(db).await {
|
|
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
|
|
|
|
if self.has_role(db, "scheckbuch").await {
|
|
days.push(Day::new_guest(db, date, false).await);
|
|
} else {
|
|
days.push(Day::new(db, date, false).await);
|
|
}
|
|
}
|
|
|
|
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
|
if self.has_role(db, "scheckbuch").await {
|
|
let day = Day::new_guest(db, date, true).await;
|
|
if !day.planned_events.is_empty() {
|
|
days.push(day);
|
|
}
|
|
} else {
|
|
days.push(Day::new(db, date, true).await);
|
|
}
|
|
}
|
|
days
|
|
}
|
|
|
|
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
|
if self.has_role(db, "cox").await {
|
|
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
|
|
//december
|
|
//has 31
|
|
//days
|
|
end_of_year
|
|
.signed_duration_since(Local::now().date_naive())
|
|
.num_days()
|
|
+ 1
|
|
} else {
|
|
6
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for User {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
match req.cookies().get_private("loggedin_user") {
|
|
Some(user_id) => match user_id.value().parse::<i32>() {
|
|
Ok(user_id) => {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
let Some(user) = User::find_by_id(db, user_id).await else {
|
|
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
|
|
};
|
|
if user.deleted {
|
|
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
|
|
}
|
|
user.logged_in(db).await;
|
|
|
|
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
|
|
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
|
|
req.cookies().add_private(cookie);
|
|
|
|
Outcome::Success(user)
|
|
}
|
|
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
|
|
},
|
|
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct TechUser {
|
|
pub(crate) user: User,
|
|
}
|
|
|
|
impl Deref for TechUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.user
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for TechUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "tech").await {
|
|
Outcome::Success(TechUser { user })
|
|
} else {
|
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct CoxUser {
|
|
user: User,
|
|
}
|
|
|
|
impl Deref for CoxUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.user
|
|
}
|
|
}
|
|
|
|
impl CoxUser {
|
|
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
|
if user.has_role(db, "cox").await {
|
|
Some(CoxUser { user })
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for CoxUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "cox").await {
|
|
Outcome::Success(CoxUser { user })
|
|
} else {
|
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct AdminUser {
|
|
pub(crate) user: User,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for AdminUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "admin").await {
|
|
Outcome::Success(AdminUser { user })
|
|
} else {
|
|
Outcome::Forward(Status::Forbidden)
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct AllowedForPlannedTripsUser(pub(crate) User);
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
|
|
Outcome::Success(AllowedForPlannedTripsUser(user))
|
|
} else {
|
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<AllowedForPlannedTripsUser> for User {
|
|
fn from(val: AllowedForPlannedTripsUser) -> Self {
|
|
val.0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct DonauLinzUser(pub(crate) User);
|
|
|
|
impl From<DonauLinzUser> for User {
|
|
fn from(val: DonauLinzUser) -> Self {
|
|
val.0
|
|
}
|
|
}
|
|
|
|
impl Deref for DonauLinzUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for DonauLinzUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "Donau Linz").await
|
|
&& !user.has_role(db, "Unterstützend").await
|
|
&& !user.has_role(db, "Förderndes Mitglied").await
|
|
{
|
|
Outcome::Success(DonauLinzUser(user))
|
|
} else {
|
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct SchnupperBetreuerUser(pub(crate) User);
|
|
|
|
impl From<SchnupperBetreuerUser> for User {
|
|
fn from(val: SchnupperBetreuerUser) -> Self {
|
|
val.0
|
|
}
|
|
}
|
|
|
|
impl Deref for SchnupperBetreuerUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for SchnupperBetreuerUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "schnupper-betreuer").await {
|
|
Outcome::Success(SchnupperBetreuerUser(user))
|
|
} else {
|
|
Outcome::Forward(Status::Forbidden)
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct VorstandUser(pub(crate) User);
|
|
|
|
impl From<VorstandUser> for User {
|
|
fn from(val: VorstandUser) -> Self {
|
|
val.0
|
|
}
|
|
}
|
|
|
|
impl Deref for VorstandUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for VorstandUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "Vorstand").await {
|
|
Outcome::Success(VorstandUser(user))
|
|
} else {
|
|
Outcome::Forward(Status::Forbidden)
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct PlannedEventUser(pub(crate) User);
|
|
|
|
impl From<PlannedEventUser> for User {
|
|
fn from(val: PlannedEventUser) -> Self {
|
|
val.0
|
|
}
|
|
}
|
|
|
|
impl Deref for PlannedEventUser {
|
|
type Target = User;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for PlannedEventUser {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if user.has_role(db, "planned_event").await {
|
|
Outcome::Success(PlannedEventUser(user))
|
|
} else {
|
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{tera::admin::user::UserEditForm, testdb};
|
|
|
|
use super::User;
|
|
use sqlx::SqlitePool;
|
|
|
|
#[sqlx::test]
|
|
fn test_find_correct_id() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
assert_eq!(user.id, 1);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_wrong_id() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1337).await;
|
|
assert!(user.is_none());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_correct_name() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_name(&pool, "admin".into()).await.unwrap();
|
|
assert_eq!(user.id, 1);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_wrong_name() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_name(&pool, "name-does-not-exist".into()).await;
|
|
assert!(user.is_none());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_all() {
|
|
let pool = testdb!();
|
|
let res = User::all(&pool).await;
|
|
assert!(res.len() > 3);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_cox() {
|
|
let pool = testdb!();
|
|
let res = User::cox(&pool).await;
|
|
assert_eq!(res.len(), 3);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_succ_create() {
|
|
let pool = testdb!();
|
|
|
|
assert_eq!(User::create(&pool, "new-user-name".into()).await, true);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_duplicate_name_create() {
|
|
let pool = testdb!();
|
|
|
|
assert_eq!(User::create(&pool, "admin".into()).await, false);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_update() {
|
|
let pool = testdb!();
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
user.update(
|
|
&pool,
|
|
UserEditForm {
|
|
id: 1,
|
|
dob: None,
|
|
weight: None,
|
|
sex: Some("m".into()),
|
|
roles: HashMap::new(),
|
|
member_since_date: None,
|
|
birthdate: None,
|
|
mail: None,
|
|
nickname: None,
|
|
notes: None,
|
|
phone: None,
|
|
address: None,
|
|
family_id: None,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
assert_eq!(user.sex, Some("m".into()));
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn succ_login_with_test_db() {
|
|
let pool = testdb!();
|
|
User::login(&pool, "admin".into(), "admin".into())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn wrong_pw() {
|
|
let pool = testdb!();
|
|
assert!(User::login(&pool, "admin".into(), "admi".into())
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn wrong_username() {
|
|
let pool = testdb!();
|
|
assert!(User::login(&pool, "admi".into(), "admin".into())
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn reset() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
user.reset_pw(&pool).await;
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
assert_eq!(user.pw, None);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn update_pw() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
assert!(User::login(&pool, "admin".into(), "abc".into())
|
|
.await
|
|
.is_err());
|
|
|
|
user.update_pw(&pool, "abc".into()).await;
|
|
|
|
User::login(&pool, "admin".into(), "abc".into())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|