diff --git a/migration.sql b/migration.sql index f130e72..0a53d23 100644 --- a/migration.sql +++ b/migration.sql @@ -2,10 +2,6 @@ CREATE TABLE IF NOT EXISTS "user" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" text NOT NULL UNIQUE, "pw" text, - "is_cox" boolean NOT NULL DEFAULT FALSE, - "is_admin" boolean NOT NULL DEFAULT FALSE, - "is_guest" boolean NOT NULL DEFAULT TRUE, - "is_tech" boolean NOT NULL DEFAULT FALSE, "deleted" boolean NOT NULL DEFAULT FALSE, "last_access" DATETIME, "dob" text, @@ -15,6 +11,17 @@ CREATE TABLE IF NOT EXISTS "user" ( "dirty_dozen" text ); +CREATE TABLE IF NOT EXISTS "role" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" text NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS "user_role" ( + "user_id" INTEGER NOT NULL REFERENCES user(id), + "role_id" INTEGER NOT NULL REFERENCES role(id), + CONSTRAINT unq UNIQUE (user_id, role_id) +); + CREATE TABLE IF NOT EXISTS "trip_type" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" text NOT NULL UNIQUE, diff --git a/seeds.sql b/seeds.sql index 1b2ca6a..9acfe7f 100644 --- a/seeds.sql +++ b/seeds.sql @@ -1,10 +1,19 @@ -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('admin', true, true, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('rower', false, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('guest', false, false, true, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('cox', true, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "role" (name) VALUES ('admin'); +INSERT INTO "role" (name) VALUES ('cox'); +INSERT INTO "role" (name) VALUES ('scheckbuch'); +INSERT INTO "role" (name) VALUES ('tech'); +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" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); +INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); +INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); INSERT INTO "user" (name) VALUES('new'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('cox2', true, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('rower2', false, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); +INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); diff --git a/src/model/boat.rs b/src/model/boat.rs index 351404b..20077b1 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -89,7 +89,7 @@ impl Boat { .ok() } - pub async fn shipmaster_allowed(&self, user: &User) -> bool { + pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool { if let Some(owner_id) = self.owner { return owner_id == user.id; } @@ -98,7 +98,23 @@ impl Boat { return true; } - user.is_cox + user.has_role(db, "cox").await + } + + pub async fn shipmaster_allowed_tx( + &self, + db: &mut Transaction<'_, Sqlite>, + user: &User, + ) -> bool { + if let Some(owner_id) = self.owner { + return owner_id == user.id; + } + + if self.amount_seats == 1 { + return true; + } + + user.has_role_tx(db, "cox").await } pub async fn is_locked(&self, db: &SqlitePool) -> bool { @@ -156,10 +172,10 @@ ORDER BY amount_seats DESC } pub async fn for_user(db: &SqlitePool, user: &User) -> Vec { - if user.is_admin { + if user.has_role(db, "admin").await { return Self::all(db).await; } - let boats = if user.is_cox { + let boats = if user.has_role(db, "cox").await { sqlx::query_as!( Boat, " diff --git a/src/model/boatdamage.rs b/src/model/boatdamage.rs index ed0dbc0..37207f2 100644 --- a/src/model/boatdamage.rs +++ b/src/model/boatdamage.rs @@ -141,7 +141,7 @@ ORDER BY created_at DESC .map_err(|e| e.to_string())?; let user = User::find_by_id(db, boat.user_id_fixed).await.unwrap(); - if user.is_tech { + if user.has_role(db, "tech").await { return self .verified( db, @@ -162,7 +162,7 @@ ORDER BY created_at DESC boat: BoatDamageVerified<'_>, ) -> Result<(), String> { if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await { - if !verifier.is_tech { + if !verifier.has_role(db, "tech").await { Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await; return Err("You are not allowed to verify the boat!".into()); } diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 4ddd818..f0909fa 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -272,7 +272,7 @@ ORDER BY departure DESC if let Ok(log_to_finalize) = TryInto::::try_into(log.clone()) { //TODO: fix clone() above - if !boat.shipmaster_allowed(created_by_user).await { + if !boat.shipmaster_allowed(db, created_by_user).await { return Err(LogbookCreateError::UserNotAllowedToUseBoat); } @@ -343,7 +343,7 @@ ORDER BY departure DESC } } - if !boat.shipmaster_allowed(created_by_user).await { + if !boat.shipmaster_allowed(db, created_by_user).await { return Err(LogbookCreateError::UserNotAllowedToUseBoat); } @@ -452,7 +452,7 @@ ORDER BY departure DESC return Err(LogbookUpdateError::SteeringPersonNotInRowers); } - if !boat.shipmaster_allowed(user).await && self.shipmaster != user.id { + if !boat.shipmaster_allowed_tx(db, user).await && self.shipmaster != user.id { //second part: shipmaster has entered a different user, then the user should be able to //`home` it return Err(LogbookUpdateError::UserNotAllowedToUseBoat); @@ -471,7 +471,7 @@ ORDER BY departure DESC return Err(LogbookUpdateError::ArrivalNotAfterDeparture); } let today = Utc::now().date_naive(); - if arr.date() != today && !user.is_admin { + if arr.date() != today && !user.has_role_tx(db, "admin").await { return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); } @@ -506,7 +506,7 @@ ORDER BY departure DESC pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { Log::create(db, format!("{user:?} deleted trip: {self:?}")).await; - if user.is_admin || user.id == self.shipmaster { + if user.has_role(db, "admin").await || user.id == self.shipmaster { sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) .execute(db) .await diff --git a/src/model/mod.rs b/src/model/mod.rs index b42ed2d..91d7df1 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -14,6 +14,7 @@ pub mod log; pub mod logbook; pub mod logtype; pub mod planned_event; +pub mod role; pub mod rower; pub mod stat; pub mod trip; diff --git a/src/model/planned_event.rs b/src/model/planned_event.rs index a1c0bf1..a4e61e8 100644 --- a/src/model/planned_event.rs +++ b/src/model/planned_event.rs @@ -6,7 +6,7 @@ use ics::{ Event, ICalendar, }; use serde::Serialize; -use sqlx::{FromRow, SqlitePool}; +use sqlx::{FromRow, SqlitePool, Row}; use super::{tripdetails::TripDetails, triptype::TripType, user::User}; @@ -47,27 +47,28 @@ pub struct Registration { impl Registration { pub async fn all_rower(db: &SqlitePool, trip_details_id: i64) -> Vec { - sqlx::query!( + sqlx::query( + &format!( r#" SELECT (SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?", user_note, user_id, (SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at, - (SELECT is_guest FROM user WHERE user_trip.user_id = user.id) as is_guest -FROM user_trip WHERE trip_details_id = ? - "#, - trip_details_id, + (SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest +FROM user_trip WHERE trip_details_id = {} + "#,trip_details_id), ) .fetch_all(db) .await .unwrap() .into_iter() - .map(|r| Registration { - name: r.name.or(r.user_note).unwrap(), //Ok, either name or user_note needs to be set - registered_at: r.registered_at, - is_guest: r.is_guest, - is_real_guest: r.user_id.is_none(), + .map(|r| + Registration { + name: r.get::, usize>(0).or(r.get::, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set + registered_at: r.get::(3), + is_guest: r.get::(4), + is_real_guest: r.get::, usize>(2).is_none(), }) .collect() } @@ -78,8 +79,7 @@ FROM user_trip WHERE trip_details_id = ? " SELECT (SELECT name FROM user WHERE cox_id = id) as name, - (SELECT created_at FROM user WHERE cox_id = id) as registered_at, - (SELECT is_guest FROM user WHERE cox_id = id) as is_guest + (SELECT created_at FROM user WHERE cox_id = id) as registered_at FROM trip WHERE planned_event_id = ? ", trip_details_id @@ -91,7 +91,7 @@ FROM trip WHERE planned_event_id = ? .map(|r| Registration { name: r.name, registered_at: r.registered_at, - is_guest: r.is_guest, + is_guest: false, is_real_guest: false, }) .collect() //Okay, as PlannedEvent can only be created with proper DB backing diff --git a/src/model/role.rs b/src/model/role.rs new file mode 100644 index 0000000..8ad982f --- /dev/null +++ b/src/model/role.rs @@ -0,0 +1,17 @@ +use serde::Serialize; +use sqlx::{FromRow, SqlitePool}; + +#[derive(FromRow, Serialize, Clone)] +pub struct Role { + id: i64, + name: String, +} + +impl Role { + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!(Role, "SELECT id, name FROM role") + .fetch_all(db) + .await + .unwrap() + } +} diff --git a/src/model/rower.rs b/src/model/rower.rs index 750c788..613b690 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -16,7 +16,7 @@ impl Rower { sqlx::query_as!( User, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) ", diff --git a/src/model/trip.rs b/src/model/trip.rs index 07adefa..e6139ee 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -276,11 +276,12 @@ mod test { fn test_new_own() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); @@ -301,11 +302,12 @@ mod test { fn test_new_succ_join() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox2".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox2".into()).await.unwrap(), + ) + .await + .unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); @@ -316,11 +318,12 @@ mod test { fn test_new_failed_join_already_cox() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox2".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox2".into()).await.unwrap(), + ) + .await + .unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); @@ -332,11 +335,12 @@ mod test { fn test_succ_update_own() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); @@ -354,11 +358,12 @@ mod test { fn test_succ_update_own_with_triptype() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); @@ -377,11 +382,12 @@ mod test { fn test_fail_update_own_not_your_trip() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox2".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox2".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); @@ -397,11 +403,12 @@ mod test { fn test_succ_delete_by_planned_event() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); @@ -419,11 +426,12 @@ mod test { fn test_succ_delete() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); @@ -436,11 +444,12 @@ mod test { fn test_fail_delete_diff_cox() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox2".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox2".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); @@ -457,11 +466,12 @@ mod test { fn test_fail_delete_someone_registered() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); diff --git a/src/model/tripdetails.rs b/src/model/tripdetails.rs index 71a427c..aeec004 100644 --- a/src/model/tripdetails.rs +++ b/src/model/tripdetails.rs @@ -120,7 +120,7 @@ ORDER BY day;", pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool { if self.belongs_to_event(db).await { - user.is_admin + user.has_role(db, "admin").await } else { self.user_is_cox(db, user).await != CoxAtTrip::No } diff --git a/src/model/user.rs b/src/model/user.rs index 064d7da..c759b51 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -21,10 +21,6 @@ pub struct User { pub id: i64, pub name: String, pub pw: Option, - pub is_cox: bool, - pub is_admin: bool, - pub is_guest: bool, - pub is_tech: bool, pub dob: Option, pub weight: Option, pub sex: Option, @@ -32,17 +28,35 @@ pub struct User { pub last_access: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct UserWithRoles { + #[serde(flatten)] + pub user: User, + pub roles: Vec, +} + +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, } 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, } } @@ -91,11 +105,56 @@ impl User { .rowed_km } + 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 { + sqlx::query!( + "SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?;", + 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 { sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE id like ? ", @@ -110,7 +169,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE id like ? ", @@ -125,7 +184,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE name like ? ", @@ -167,7 +226,7 @@ WHERE name like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -182,7 +241,7 @@ ORDER BY last_access DESC sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' ORDER BY name @@ -197,9 +256,9 @@ ORDER BY name sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +SELECT id, name, pw, deleted, last_access, dob, weight, sex FROM user -WHERE deleted = 0 AND is_cox=true +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 " ) @@ -208,24 +267,16 @@ ORDER BY last_access DESC .unwrap() } - pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool { - sqlx::query!( - "INSERT INTO USER(name, is_guest) VALUES (?,?)", - name, - is_guest, - ) - .execute(db) - .await - .is_ok() + 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) { sqlx::query!( - "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ?, dob = ?, weight = ?, sex = ? where id = ?", - data.is_cox, - data.is_admin, - data.is_guest, - data.is_tech, + "UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?", data.dob, data.weight, data.sex, @@ -234,6 +285,23 @@ ORDER BY last_access DESC .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() { + sqlx::query!( + "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", + self.id, + role_id + ) + .execute(db) + .await + .unwrap(); + } } pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result { @@ -309,18 +377,18 @@ ORDER BY last_access DESC pub async fn get_days(&self, db: &SqlitePool) -> Vec { let mut days = Vec::new(); - for i in 0..self.amount_days_to_show() { + for i in 0..self.amount_days_to_show(db).await { let date = (Local::now() + chrono::Duration::days(i)).date_naive(); - if self.is_guest { + 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() - 1).await { - if self.is_guest { + 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); @@ -332,8 +400,8 @@ ORDER BY last_access DESC days } - fn amount_days_to_show(&self) -> i64 { - if self.is_cox { + 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 @@ -393,28 +461,21 @@ impl Deref for TechUser { } } -impl TryFrom for TechUser { - type Error = LoginError; - - fn try_from(user: User) -> Result { - if user.is_tech { - Ok(TechUser { user }) - } else { - Err(LoginError::NotATech) - } - } -} - #[async_trait] impl<'r> FromRequest<'r> for TechUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { - Outcome::Success(user) => match user.try_into() { - Ok(user) => Outcome::Success(user), - Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotACox)), - }, + Outcome::Success(user) => { + if user.has_role(db, "tech").await { + Outcome::Success(TechUser { user }) + } else { + Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + } + } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } @@ -433,14 +494,12 @@ impl Deref for CoxUser { } } -impl TryFrom for CoxUser { - type Error = LoginError; - - fn try_from(user: User) -> Result { - if user.is_cox { - Ok(CoxUser { user }) +impl CoxUser { + pub async fn new(db: &SqlitePool, user: User) -> Option { + if user.has_role(db, "cox").await { + Some(CoxUser { user }) } else { - Err(LoginError::NotACox) + None } } } @@ -450,11 +509,16 @@ impl<'r> FromRequest<'r> for CoxUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { - Outcome::Success(user) => match user.try_into() { - Ok(user) => Outcome::Success(user), - Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotACox)), - }, + Outcome::Success(user) => { + if user.has_role(db, "cox").await { + Outcome::Success(CoxUser { user }) + } else { + Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + } + } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } @@ -466,28 +530,20 @@ pub struct AdminUser { pub(crate) user: User, } -impl TryFrom for AdminUser { - type Error = LoginError; - - fn try_from(user: User) -> Result { - if user.is_admin { - Ok(AdminUser { user }) - } else { - Err(LoginError::NotAnAdmin) - } - } -} - #[async_trait] impl<'r> FromRequest<'r> for AdminUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); match User::from_request(req).await { - Outcome::Success(user) => match user.try_into() { - Ok(user) => Outcome::Success(user), - Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotAnAdmin)), - }, + Outcome::Success(user) => { + if user.has_role(db, "admin").await { + Outcome::Success(AdminUser { user }) + } else { + Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + } + } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } @@ -499,28 +555,20 @@ pub struct NonGuestUser { pub(crate) user: User, } -impl TryFrom for NonGuestUser { - type Error = LoginError; - - fn try_from(user: User) -> Result { - if user.is_guest { - Err(LoginError::GuestNotAllowed) - } else { - Ok(NonGuestUser { user }) - } - } -} - #[async_trait] impl<'r> FromRequest<'r> for NonGuestUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); match User::from_request(req).await { - Outcome::Success(user) => match user.try_into() { - Ok(user) => Outcome::Success(user), - Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotAnAdmin)), - }, + Outcome::Success(user) => { + if !user.has_role(db, "scheckbuch").await { + Outcome::Success(NonGuestUser { user }) + } else { + Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + } + } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } @@ -529,6 +577,8 @@ impl<'r> FromRequest<'r> for NonGuestUser { #[cfg(test)] mod test { + use std::collections::HashMap; + use crate::{tera::admin::user::UserEditForm, testdb}; use super::User; @@ -580,17 +630,14 @@ mod test { fn test_succ_create() { let pool = testdb!(); - assert_eq!( - User::create(&pool, "new-user-name".into(), false).await, - true - ); + 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(), false).await, false); + assert_eq!(User::create(&pool, "admin".into()).await, false); } #[sqlx::test] @@ -602,19 +649,17 @@ mod test { &pool, UserEditForm { id: 1, - is_guest: false, - is_cox: false, - is_admin: false, - is_tech: false, dob: None, weight: None, - sex: None, + sex: Some("m".into()), + roles: HashMap::new(), }, ) .await; let user = User::find_by_id(&pool, 1).await.unwrap(); - assert_eq!(user.is_admin, false); + + assert_eq!(user.sex, Some("m".into())); } #[sqlx::test] diff --git a/src/model/usertrip.rs b/src/model/usertrip.rs index de133ed..f727bf8 100644 --- a/src/model/usertrip.rs +++ b/src/model/usertrip.rs @@ -20,7 +20,7 @@ impl UserTrip { return Err(UserTripError::DetailsLocked); } - if user.is_guest && !trip_details.allow_guests { + if user.has_role(db, "scheckbuch").await && !trip_details.allow_guests { return Err(UserTripError::GuestNotAllowedForThisEvent); } @@ -211,11 +211,12 @@ mod test { fn test_fail_create_is_cox_planned_event() { let pool = testdb!(); - let cox: CoxUser = User::find_by_name(&pool, "cox".into()) - .await - .unwrap() - .try_into() - .unwrap(); + let cox = CoxUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); Trip::new_join(&pool, &cox, &planned_event).await.unwrap(); diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs index 6ff6200..d8776f6 100644 --- a/src/tera/admin/boat.rs +++ b/src/tera/admin/boat.rs @@ -1,7 +1,7 @@ use crate::model::{ boat::{Boat, BoatToAdd, BoatToUpdate}, location::Location, - user::{AdminUser, User}, + user::{AdminUser, User, UserWithRoles}, }; use rocket::{ form::Form, @@ -30,7 +30,10 @@ async fn index( context.insert("boats", &boats); context.insert("locations", &locations); context.insert("users", &users); - context.insert("loggedin_user", &admin.user); + context.insert( + "loggedin_user", + &UserWithRoles::from_user(admin.user, db).await, + ); Template::render("admin/boat/index", context.into_json()) } diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index f0a719c..e688445 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -1,4 +1,10 @@ -use crate::model::user::{AdminUser, User}; +use std::collections::HashMap; + +use crate::model::{ + role::Role, + user::{AdminUser, User, UserWithRoles}, +}; +use futures::future::join_all; use rocket::{ form::Form, get, post, @@ -15,14 +21,26 @@ async fn index( admin: AdminUser, flash: Option>, ) -> Template { - let users = User::all(db).await; + let user_futures: Vec<_> = User::all(db) + .await + .into_iter() + .map(|u| async move { UserWithRoles::from_user(u, db).await }) + .collect(); + + let users: Vec = join_all(user_futures).await; + + let roles = Role::all(db).await; let mut context = Context::new(); if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } context.insert("users", &users); - context.insert("loggedin_user", &admin.user); + context.insert("roles", &roles); + context.insert( + "loggedin_user", + &UserWithRoles::from_user(admin.user, db).await, + ); Template::render("admin/user/index", context.into_json()) } @@ -57,16 +75,13 @@ async fn delete(db: &State, _admin: AdminUser, user: i32) -> Flash, pub(crate) weight: Option, pub(crate) sex: Option, + pub(crate) roles: HashMap, } #[post("/user", data = "")] @@ -91,7 +106,6 @@ async fn update( #[derive(FromForm)] struct UserAddForm<'r> { name: &'r str, - is_guest: bool, } #[post("/user/new", data = "")] @@ -100,7 +114,7 @@ async fn create( data: Form>, _admin: AdminUser, ) -> Flash { - if User::create(db, data.name, data.is_guest).await { + if User::create(db, data.name).await { Flash::success(Redirect::to("/admin/user"), "Successfully created user") } else { Flash::error( diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs index 89260cd..c798791 100644 --- a/src/tera/boatdamage.rs +++ b/src/tera/boatdamage.rs @@ -13,7 +13,7 @@ use crate::{ model::{ boat::Boat, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, - user::{CoxUser, NonGuestUser, TechUser, User}, + user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, }, tera::log::KioskCookie, }; @@ -57,7 +57,10 @@ async fn index( context.insert("boatdamages", &boatdamages); context.insert("boats", &boats); - context.insert("loggedin_user", &user.user); + context.insert( + "loggedin_user", + &UserWithRoles::from_user(user.user, db).await, + ); Template::render("boatdamages", context.into_json()) } diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs index 8b00446..8818d6b 100644 --- a/src/tera/ergo.rs +++ b/src/tera/ergo.rs @@ -18,7 +18,7 @@ use tera::Context; use crate::model::{ log::Log, - user::{AdminUser, User}, + user::{AdminUser, User, UserWithRoles}, }; #[derive(Serialize)] @@ -51,7 +51,7 @@ async fn send(db: &State, _user: AdminUser) -> Template { Template::render( "ergo.final", - context!(loggedin_user: &_user.user, thirty, dozen), + context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen), ) } @@ -120,7 +120,7 @@ async fn index(db: &State, user: User, flash: Option, user: NonGuestUser) -> Template { let logs = Logbook::completed(db).await; - Template::render("log.completed", context!(logs, loggedin_user: &user.user)) + Template::render( + "log.completed", + context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), + ) } #[get("/show")] @@ -232,7 +238,7 @@ async fn create_kiosk( ) .await; - create_logbook(db, data, &NonGuestUser::try_from(creator).unwrap()).await //TODO: fixme + create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme } async fn home_logbook( @@ -279,12 +285,11 @@ async fn home_kiosk( db, data, logbook_id, - &NonGuestUser::try_from( - User::find_by_id(db, logbook.shipmaster as i32) + &NonGuestUser { + user: User::find_by_id(db, logbook.shipmaster as i32) .await .unwrap(), //TODO: fixme - ) - .unwrap(), + }, ) .await } diff --git a/src/tera/mod.rs b/src/tera/mod.rs index c917262..de9ff8b 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -16,7 +16,7 @@ use crate::model::{ log::Log, tripdetails::TripDetails, triptype::TripType, - user::User, + user::{User, UserWithRoles}, usertrip::{UserTrip, UserTripDeleteError, UserTripError}, }; @@ -47,7 +47,7 @@ async fn wikiauth(db: &State, login: Form>) -> String async fn index(db: &State, user: User, flash: Option>) -> Template { let mut context = Context::new(); - if user.is_cox || user.is_admin { + if user.has_role(db, "cox").await || user.has_role(db, "admin").await { let triptypes = TripType::all(db).await; context.insert("trip_types", &triptypes); } @@ -57,8 +57,8 @@ async fn index(db: &State, user: User, flash: Option, user: NonGuestUser, year: Option, user: NonGuestUser, year: Option) -> Template::render( "stat.people", - context!(loggedin_user: &user.user, stat, personal, kiosk, guest_km), + context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km), ) } diff --git a/staging-diff.sql b/staging-diff.sql index e69de29..293af46 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "role" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" text NOT NULL UNIQUE +); +INSERT INTO "role" (name) VALUES ('admin'); +INSERT INTO "role" (name) VALUES ('cox'); +INSERT INTO "role" (name) VALUES ('scheckbuch'); +INSERT INTO "role" (name) VALUES ('tech'); + +CREATE TABLE IF NOT EXISTS "user_role" ( + "user_id" INTEGER NOT NULL REFERENCES user(id), + "role_id" INTEGER NOT NULL REFERENCES role(id), + CONSTRAINT unq UNIQUE (user_id, role_id) +); diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 5e748ac..a23ec65 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -18,10 +18,6 @@ -
- -
@@ -53,10 +49,9 @@ {% endif %}
- {{ macros::checkbox(label='Scheckbuch', name='is_guest', id=loop.index , checked=user.is_guest) }} - {{ macros::checkbox(label='Steuerberechtigter', name='is_cox', id=loop.index , checked=user.is_cox) }} - {{ macros::checkbox(label='Technical', name='is_tech', id=loop.index , checked=user.is_tech) }} - {{ macros::checkbox(label='Admin', name='is_admin', id=loop.index , checked=user.is_admin) }} + {% for role in roles %} + {{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles) }} + {% endfor%} {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }} {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }} {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }} diff --git a/templates/boatdamages.html.tera b/templates/boatdamages.html.tera index 49e2d2f..391f166 100644 --- a/templates/boatdamages.html.tera +++ b/templates/boatdamages.html.tera @@ -57,10 +57,10 @@ {% if boatdamage.fixed_at %} Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }} {% else %} - {% if loggedin_user.is_cox %} + {% if "cox" in loggedin_user.roles %}
- {% if loggedin_user.is_tech %} + {% if "tech" in loggedin_user.roles %} {% else %} @@ -72,7 +72,7 @@ {% if boatdamage.verified_at %} Verifziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }} {% else %} - {% if loggedin_user.is_tech and boatdamage.fixed_at %} + {% if "tech" in loggedin_user.roles and boatdamage.fixed_at %} diff --git a/templates/ergo.html.tera b/templates/ergo.html.tera index 71e20db..be69f49 100644 --- a/templates/ergo.html.tera +++ b/templates/ergo.html.tera @@ -132,7 +132,7 @@
- {% if loggedin_user.is_admin %} + {% if "admin" in loggedin_user.roles %}

Update

diff --git a/templates/includes/buttons.html.tera b/templates/includes/buttons.html.tera index eb91a1f..5af6043 100644 --- a/templates/includes/buttons.html.tera +++ b/templates/includes/buttons.html.tera @@ -1,4 +1,4 @@ -{% if loggedin_user.is_cox %} +{% if "cox" in loggedin_user.roles %}
{% endif %} - {% if not loggedin_user.is_guest %} + {% if "scheckbuch" not in loggedin_user.roles %} Bootsauswertung - {% if loggedin_user.is_admin %} + {% if "admin" in loggedin_user.roles %}
{% endif %} - {% if loggedin_user.is_admin %} + {% if "admin" in loggedin_user.roles %} 0 %} {% set amount_cur_rower = planned_event.rower | length %} - {{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing=loggedin_user.is_admin) }} + {{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }} {% endif %} {# --- END List Rowers --- #} - {% if loggedin_user.is_admin %} + {% if "admin" in loggedin_user.roles %} {{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }} @@ -127,7 +127,7 @@
Gäste willkommen!
{% endif %} - {% if loggedin_user.is_admin %} + {% if "admin" in loggedin_user.roles %} {# --- START Edit Form --- #}
@@ -258,9 +258,9 @@
{# --- START Add Buttons --- #} - {% if loggedin_user.is_admin or loggedin_user.is_cox %} -
-{% if loggedin_user.is_cox %} +{% if "cox" in loggedin_user.roles %} {% include "forms/trip" %} {% endif %} -{% if loggedin_user.is_admin %} +{% if "admin" in loggedin_user.roles %} {% include "forms/event" %} {% endif %}{% endblock content %} diff --git a/templates/log.html.tera b/templates/log.html.tera index 24d54ee..7ed60bd 100644 --- a/templates/log.html.tera +++ b/templates/log.html.tera @@ -24,7 +24,7 @@

Neue Ausfahrt

- {{ log::new(only_ones=loggedin_user.is_cox==false, shipmaster=loggedin_user.id) }} + {{ log::new(only_ones="cox" not in loggedin_user.roles, shipmaster=loggedin_user.id) }}
@@ -33,7 +33,7 @@ {% if on_water | length > 0 %} {% for log in on_water %} {% if log.shipmaster == loggedin_user.id %} - {{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=loggedin_user.is_cox==false) }} + {{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }} {% else %} {{ log::show(log=log, state="on_water", only_ones=true) }} {% endif %}