Compare commits

...

6 Commits

Author SHA1 Message Date
2e3be062c6 Merge pull request 'staging' (#178) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 4m40s
Reviewed-on: #178
2024-01-22 22:24:24 +01:00
6d61d1f8bc Merge branch 'main' into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m37s
CI/CD Pipeline / deploy-staging (push) Successful in 4m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 22:08:39 +01:00
b7499bd6cb clean code; if logged out: save url where user tried to go to, go there once logged in, Fixes #179
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-22 22:08:05 +01:00
60b9a4dbba adapt unit tests to account for new redirect
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m16s
CI/CD Pipeline / deploy-staging (push) Successful in 4m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 20:55:05 +01:00
a1a5e2ad89 move managing events to own role
Some checks failed
CI/CD Pipeline / test (push) Failing after 38m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 19:27:22 +01:00
eda072c713 enable changing paid state
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m15s
CI/CD Pipeline / deploy-staging (push) Successful in 4m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 19:05:18 +01:00
15 changed files with 261 additions and 121 deletions

View File

@ -15,7 +15,6 @@ test('cox can create and delete trip', async ({ page }) => {
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.getByRole('spinbutton').fill('5');
await page.getByRole('button', { name: 'Erstellen', exact: true }).click();
await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await expect(page.locator('body')).toContainText('18:00 Uhr (cox) Details');
await page.goto('http://localhost:8000/planned');
@ -57,7 +56,6 @@ test.describe('cox can edit trips', () => {
await sharedPage.locator('#sidebar #notes').click();
await sharedPage.locator('#sidebar #notes').fill('Meine Anmerkung');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await sharedPage.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Meine Anmerkung');
@ -94,7 +92,6 @@ test.describe('cox can edit trips', () => {
await sharedPage.getByRole('spinbutton').fill('3');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
await sharedPage.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
});
test('call off trip', async () => {
@ -105,7 +102,6 @@ test.describe('cox can edit trips', () => {
await sharedPage.getByRole('spinbutton').fill('0');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
await sharedPage.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await expect(sharedPage.locator('body')).toContainText('(Absage cox )');
});

View File

@ -3,10 +3,12 @@ INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('planned_event');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,6);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');

View File

@ -1,12 +1,8 @@
use std::error::Error;
use lettre::{
message::{
header::{self, ContentType},
MultiPart, SinglePart,
},
transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
use sqlx::SqlitePool;
@ -17,7 +13,7 @@ use super::{family::Family, role::Role, user::User};
pub struct Mail {}
impl Mail {
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
pub async fn send(db: &SqlitePool, data: MailToSend, smtp_pw: String) -> bool {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"

View File

@ -3,8 +3,8 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize, Clone)]
pub struct Role {
id: i64,
name: String,
pub(crate) id: i64,
pub(crate) name: String,
}
impl Role {
@ -30,6 +30,21 @@ WHERE id like ?
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM role
WHERE name like ?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!(
"SELECT u.mail

View File

@ -2,7 +2,6 @@ use std::ops::{Deref, DerefMut};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use chrono::{Datelike, Local, NaiveDate};
use chrono_tz::Etc::UTC;
use log::info;
use rocket::{
async_trait,
@ -14,7 +13,7 @@ use rocket::{
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{family::Family, log::Log, tripdetails::TripDetails, Day};
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000;
@ -101,10 +100,12 @@ pub enum LoginError {
}
#[derive(Debug, Serialize)]
pub(crate) struct Fee {
pub(crate) sum_in_cents: i32,
pub(crate) parts: Vec<(String, i32)>,
pub(crate) name: String,
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
}
impl Fee {
@ -113,6 +114,8 @@ impl Fee {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
paid: false,
}
}
@ -122,8 +125,18 @@ impl Fee {
self.parts.push((desc, price_in_cents));
}
pub fn name(&mut self, name: String) {
self.name = name;
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push_str("&");
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
@ -142,22 +155,23 @@ impl User {
let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
let mut names = String::new();
for member in family.members(db).await {
if !names.is_empty() {
names.push_str(" + ");
fee.add_person(&member);
if member.has_role(db, "paid").await {
fee.paid();
}
names.push_str(&member.name);
fee.merge(member.fee_without_families(db).await);
}
fee.name(names);
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.name(self.name.clone());
fee.add_person(&self);
if self.has_role(db, "paid").await {
fee.paid();
}
fee.merge(self.fee_without_families(db).await);
}
@ -452,17 +466,38 @@ ORDER BY last_access DESC
.unwrap();
for role_id in data.roles.into_keys() {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role_id
self.add_role(
db,
&Role::find_by_id(db, role_id.parse::<i32>().unwrap())
.await
.unwrap(),
)
.execute(db)
.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 {
@ -813,6 +848,43 @@ impl<'r> FromRequest<'r> for VorstandUser {
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlannedEventUser(pub(crate) User);
impl Into<User> for PlannedEventUser {
fn into(self) -> User {
self.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;

View File

@ -1,4 +1,4 @@
use rocket::{form::Form, fs::FileServer, post, routes, Build, FromForm, Rocket, State};
use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use serde_json::json;
use sqlx::SqlitePool;

View File

@ -1,5 +1,4 @@
use rocket::form::Form;
use rocket::fs::TempFile;
use rocket::response::{Flash, Redirect};
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket::{post, FromForm};
@ -34,28 +33,24 @@ async fn index(
}
#[get("/mail/fee")]
async fn fee(
db: &State<SqlitePool>,
admin: AdminUser,
config: &State<Config>,
flash: Option<FlashMessage<'_>>,
) -> &'static str {
async fn fee(db: &State<SqlitePool>, _admin: AdminUser, config: &State<Config>) -> &'static str {
Mail::fees(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[derive(FromForm, Debug)]
pub struct MailToSend<'a> {
pub struct MailToSend {
//<'a> {
pub(crate) role_id: i32,
pub(crate) subject: String,
pub(crate) body: String,
pub(crate) files: Vec<TempFile<'a>>,
//pub(crate) files: Vec<TempFile<'a>>,
}
#[post("/mail", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<MailToSend<'_>>,
data: Form<MailToSend>,
config: &State<Config>,
_admin: AdminUser,
) -> Flash<Redirect> {

View File

@ -10,7 +10,7 @@ use sqlx::SqlitePool;
use crate::model::{
planned_event::PlannedEvent,
tripdetails::{TripDetails, TripDetailsToAdd},
user::AdminUser,
user::PlannedEventUser,
};
//TODO: add constraints (e.g. planned_amount_cox > 0)
@ -25,7 +25,7 @@ struct AddPlannedEventForm<'r> {
async fn create(
db: &State<SqlitePool>,
data: Form<AddPlannedEventForm<'_>>,
_admin: AdminUser,
_admin: PlannedEventUser,
) -> Flash<Redirect> {
let data = data.into_inner();
@ -36,7 +36,7 @@ async fn create(
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
Flash::success(Redirect::to("/"), "Event hinzugefügt")
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
}
//TODO: add constraints (e.g. planned_amount_cox > 0)
@ -54,7 +54,7 @@ struct UpdatePlannedEventForm<'r> {
async fn update(
db: &State<SqlitePool>,
data: Form<UpdatePlannedEventForm<'_>>,
_admin: AdminUser,
_admin: PlannedEventUser,
) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, data.id).await {
Some(planned_event) => {
@ -68,20 +68,20 @@ async fn update(
data.is_locked,
)
.await;
Flash::success(Redirect::to("/"), "Successfully edited the event")
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
}
None => Flash::error(Redirect::to("/"), "Planned event id not found"),
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
}
}
#[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: AdminUser) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, id).await {
Some(planned_event) => {
planned_event.delete(db).await;
Flash::success(Redirect::to("/"), "Event gelöscht")
Flash::success(Redirect::to("/planned"), "Event gelöscht")
}
None => Flash::error(Redirect::to("/"), "PlannedEvent does not exist"),
None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
}
}
@ -120,7 +120,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -151,7 +151,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -187,7 +187,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -196,7 +196,7 @@ mod test {
assert_eq!(
flash_cookie.value(),
"7:successSuccessfully edited the event"
"7:successEvent erfolgreich bearbeitet"
);
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
@ -224,7 +224,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -255,7 +255,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()

View File

@ -3,9 +3,9 @@ use std::collections::HashMap;
use crate::model::{
family::Family,
role::Role,
user::{AdminUser, Fee, User, UserWithRoles, VorstandUser},
user::{AdminUser, User, UserWithRoles, VorstandUser},
};
use futures::future::{self, join_all};
use futures::future::join_all;
use rocket::{
form::Form,
get, post,
@ -77,6 +77,33 @@ async fn fees(
Template::render("admin/user/fees", context.into_json())
}
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
_admin: AdminUser,
user_ids: Vec<i32>,
) -> Flash<Redirect> {
let mut res = String::new();
for user_id in user_ids {
let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await {
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
} else {
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
}
}
res.truncate(res.len() - 3); // remove ' + ' from the end
Flash::success(
Redirect::to("/admin/user/fees"),
format!("Zahlungsstatus von {} erfolgreich geändert", res),
)
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
@ -165,5 +192,5 @@ async fn create(
}
pub fn routes() -> Vec<Route> {
routes![index, resetpw, update, create, delete, fees]
routes![index, resetpw, update, create, delete, fees, fees_paid]
}

View File

@ -89,7 +89,15 @@ async fn login(
)
.await;
Flash::success(Redirect::to("/"), "Login erfolgreich")
// Check for redirect_url cookie and redirect accordingly
match cookies.get_private("redirect_url") {
Some(redirect_cookie) => {
let redirect_url = redirect_cookie.value().to_string();
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
}
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
}
}
#[get("/set-pw/<userid>")]

View File

@ -34,7 +34,7 @@ async fn create(
//)
//.await;
Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.")
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
}
#[derive(FromForm)]
@ -66,16 +66,19 @@ async fn update(
)
.await
{
Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."),
Ok(_) => Flash::success(
Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.",
),
Err(TripUpdateError::NotYourTrip) => {
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
}
Err(TripUpdateError::TripDetailsDoesNotExist) => {
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
}
}
} else {
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
}
}
@ -92,21 +95,21 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
),
)
.await;
Flash::success(Redirect::to("/"), "Danke für's helfen!")
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits aus!")
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
}
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
Redirect::to("/"),
Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!",
),
Err(CoxHelpError::DetailsLocked) => {
Flash::error(Redirect::to("/"), "Boot ist bereits eingeteilt.")
Flash::error(Redirect::to("/planned"), "Boot ist bereits eingeteilt.")
}
}
} else {
Flash::error(Redirect::to("/"), "Event gibt's nicht")
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
}
}
@ -114,18 +117,18 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
let trip = Trip::find_by_id(db, trip_id).await;
match trip {
None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"),
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
Some(trip) => match trip.delete(db, &cox).await {
Ok(_) => {
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!")
Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!")
}
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
Redirect::to("/"),
Redirect::to("/planned"),
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
),
Err(TripDeleteError::NotYourTrip) => {
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
}
},
}
@ -145,17 +148,17 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(TripHelpDeleteError::DetailsLocked) => {
Flash::error(Redirect::to("/"), "Boot bereits eingeteilt")
Flash::error(Redirect::to("/planned"), "Boot bereits eingeteilt")
}
Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...")
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
}
}
} else {
Flash::error(Redirect::to("/"), "Planned_event does not exist.")
Flash::error(Redirect::to("/planned"), "Planned_event does not exist.")
}
}
@ -202,7 +205,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -250,7 +253,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -288,7 +291,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -326,7 +329,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -354,7 +357,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -367,7 +370,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -398,7 +401,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -429,7 +432,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -470,7 +473,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -498,7 +501,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
@ -526,7 +529,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()

View File

@ -125,7 +125,7 @@ async fn new_kiosk(
async fn kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
kiosk: KioskCookie,
_kiosk: KioskCookie,
) -> Template {
let boats = Boat::all(db).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(

View File

@ -3,10 +3,14 @@ use rocket::{
fairing::AdHoc,
form::Form,
fs::FileServer,
get, post,
get,
http::Cookie,
post,
request::FlashMessage,
response::{Flash, Redirect},
routes, Build, FromForm, Rocket, State,
routes,
time::{Duration, OffsetDateTime},
Build, FromForm, Request, Rocket, State,
};
use rocket_dyn_templates::Template;
use serde::Deserialize;
@ -51,7 +55,13 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
}
#[catch(401)] //Unauthorized
fn unauthorized_error() -> Redirect {
fn unauthorized_error(req: &Request) -> Redirect {
// Save the URL the user tried to access, to be able to go there once logged in
let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
println!("{}", req.uri());
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
req.cookies().add_private(redirect_cookie);
Redirect::to("/auth")
}

View File

@ -3,29 +3,45 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="my-3") }}
{% endif %}
<h1 class="h1">Gebühren</h1>
<!-- START filterBar -->
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search" name="name" id="filter-js" class="search-bar" placeholder="Suchen nach Name"/>
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js" class="text-primary-950 dark:text-white text-right"></div>
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Gebühren</h2>
<div class="text-sm p-3">
{% for fee in fees | sort(attribute="name") %}
<b>{{ fee.name }}: {{ fee.sum_in_cents / 100 }}€</b><br />
{% for p in fee.parts %}
{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}
{% endfor %}
<hr />
<div {% if fee.paid %} style="background-color: green;" {% endif %} data-filterable="true" data-filter="{{ fee.name }}" class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="grid sm:grid-cols-1 gap-3">
<div style="width: 100%" class="col-span-2">
<b>{{ fee.name }}</b>
</div>
<div style="width: 100%">
{{ fee.sum_in_cents / 100 }}€:
</div>
<div style="width: 100%">
{% for p in fee.parts %}
{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock content%}
{% endblock content %}

View File

@ -304,9 +304,9 @@
</div>
{# --- START Add Buttons --- #}
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "admin" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "planned_event" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "planned_event" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
@ -316,7 +316,7 @@
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "planned_event" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
@ -335,6 +335,6 @@
{% include "forms/trip" %}
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}