From 4588b634c165e8feeef99ad1639af990ee3f4e2d Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Sun, 19 May 2019 19:00:56 +0200 Subject: [PATCH] frontend: allow admins to change a users role --- src/core/db.rs | 1 + src/core/entities.rs | 2 +- src/core/error.rs | 8 +- src/core/usecases/archive_comments.rs | 14 +- src/core/usecases/archive_ratings.rs | 16 ++- src/core/usecases/change_user_role.rs | 26 ++++ src/core/usecases/mod.rs | 7 +- src/infrastructure/flows/archive_comments.rs | 30 +++- src/infrastructure/flows/archive_ratings.rs | 22 ++- src/infrastructure/flows/change_user_role.rs | 73 ++++++++++ src/infrastructure/flows/mod.rs | 32 ++++- src/ports/web/frontend/login.rs | 7 +- src/ports/web/frontend/main.css | 8 +- src/ports/web/frontend/mod.rs | 140 +++++++++++++------ src/ports/web/frontend/view/dashboard.rs | 13 +- src/ports/web/frontend/view/mod.rs | 65 +++++++++ src/ports/web/guards.rs | 35 ----- 17 files changed, 388 insertions(+), 111 deletions(-) create mode 100644 src/core/usecases/change_user_role.rs create mode 100644 src/infrastructure/flows/change_user_role.rs diff --git a/src/core/db.rs b/src/core/db.rs index bc68d15a..c6c2c939 100644 --- a/src/core/db.rs +++ b/src/core/db.rs @@ -39,6 +39,7 @@ pub trait UserGateway { fn create_user(&self, user: User) -> Result<()>; fn update_user(&self, user: &User) -> Result<()>; fn get_user(&self, username: &str) -> Result; + //TODO make email => user relation unique fn get_users_by_email(&self, email: &str) -> Result>; fn all_users(&self) -> Result>; fn delete_user(&self, username: &str) -> Result<()>; diff --git a/src/core/entities.rs b/src/core/entities.rs index 2f4b05fe..1ed6f76a 100644 --- a/src/core/entities.rs +++ b/src/core/entities.rs @@ -115,7 +115,7 @@ pub struct User { } #[rustfmt::skip] -#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, ToPrimitive)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, FromPrimitive, ToPrimitive)] pub enum Role { Guest = 0, User = 1, diff --git a/src/core/error.rs b/src/core/error.rs index 4dd86d23..38fb8334 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -32,7 +32,10 @@ quick_error! { description("Invalid username") } UserExists{ - description("The user already exits") + description("The user already exists") + } + UserDoesNotExist{ + description("The user does not exist") } Password{ description("Invalid password") @@ -73,6 +76,9 @@ quick_error! { InvalidLimit{ description("Invalid limit") } + Role{ + description("Invalid role") + } } } diff --git a/src/core/usecases/archive_comments.rs b/src/core/usecases/archive_comments.rs index 261bdbe2..33c2212a 100644 --- a/src/core/usecases/archive_comments.rs +++ b/src/core/usecases/archive_comments.rs @@ -1,8 +1,14 @@ use crate::core::prelude::*; -pub fn archive_comments(db: &D, ids: &[&str]) -> Result<()> { +pub fn archive_comments(db: &D, user_email: &str, ids: &[&str]) -> Result<()> { info!("Archiving {} comments", ids.len()); - let archived = Timestamp::now(); - db.archive_comments(ids, archived)?; - Ok(()) + let users = db.get_users_by_email(user_email)?; + if let Some(user) = users.first() { + if user.role >= Role::Scout { + let archived = Timestamp::now(); + db.archive_comments(ids, archived)?; + return Ok(()); + } + } + Err(ParameterError::Forbidden.into()) } diff --git a/src/core/usecases/archive_ratings.rs b/src/core/usecases/archive_ratings.rs index 5c291a2f..9ecb11b3 100644 --- a/src/core/usecases/archive_ratings.rs +++ b/src/core/usecases/archive_ratings.rs @@ -1,9 +1,15 @@ use crate::core::prelude::*; -pub fn archive_ratings(db: &D, ids: &[&str]) -> Result<()> { +pub fn archive_ratings(db: &D, user_email: &str, ids: &[&str]) -> Result<()> { debug!("Archiving ratings {:?}", ids); - let archived = Timestamp::now(); - db.archive_comments_of_ratings(ids, archived)?; - db.archive_ratings(ids, archived)?; - Ok(()) + let users = db.get_users_by_email(user_email)?; + if let Some(user) = users.first() { + if user.role >= Role::Scout { + let archived = Timestamp::now(); + db.archive_comments_of_ratings(ids, archived)?; + db.archive_ratings(ids, archived)?; + return Ok(()); + } + } + Err(ParameterError::Forbidden.into()) } diff --git a/src/core/usecases/change_user_role.rs b/src/core/usecases/change_user_role.rs new file mode 100644 index 00000000..852a5b26 --- /dev/null +++ b/src/core/usecases/change_user_role.rs @@ -0,0 +1,26 @@ +use crate::core::prelude::*; + +pub fn change_user_role( + db: &D, + account_email: &str, + user_email: &str, + role: Role, +) -> Result<()> { + info!("Changing role to {:?} for {}", role, user_email); + let accounts = db.get_users_by_email(account_email)?; + let users = db.get_users_by_email(user_email)?; + let account = accounts + .first() + .ok_or_else(|| ParameterError::UserDoesNotExist)?; + let mut user = users + .first() + .ok_or_else(|| ParameterError::UserDoesNotExist)? + .to_owned(); + if account.role > user.role && role < account.role { + user.role = role; + db.update_user(&user)?; + Ok(()) + } else { + Err(ParameterError::Forbidden.into()) + } +} diff --git a/src/core/usecases/mod.rs b/src/core/usecases/mod.rs index 37059a5c..615dc38e 100644 --- a/src/core/usecases/mod.rs +++ b/src/core/usecases/mod.rs @@ -13,6 +13,7 @@ mod archive_comments; mod archive_entries; mod archive_events; mod archive_ratings; +mod change_user_role; mod confirm_email; mod create_new_entry; mod create_new_event; @@ -32,9 +33,9 @@ mod update_event; pub use self::{ archive_comments::*, archive_entries::*, archive_events::*, archive_ratings::*, - confirm_email::*, create_new_entry::*, create_new_event::*, create_new_user::*, - delete_event::*, find_duplicates::*, indexing::*, login::*, query_events::*, rate_entry::*, - register::*, search::*, update_entry::*, update_event::*, + change_user_role::*, confirm_email::*, create_new_entry::*, create_new_event::*, + create_new_user::*, delete_event::*, find_duplicates::*, indexing::*, login::*, + query_events::*, rate_entry::*, register::*, search::*, update_entry::*, update_event::*, }; pub fn load_ratings_with_comments( diff --git a/src/infrastructure/flows/archive_comments.rs b/src/infrastructure/flows/archive_comments.rs index 5b459fb1..33d54558 100644 --- a/src/infrastructure/flows/archive_comments.rs +++ b/src/infrastructure/flows/archive_comments.rs @@ -1,13 +1,17 @@ use super::*; - use diesel::connection::Connection; -pub fn archive_comments(connections: &sqlite::Connections, ids: &[&str]) -> Result<()> { +pub fn archive_comments( + connections: &sqlite::Connections, + account_email: &str, + ids: &[&str], +) -> Result<()> { + //TODO: check if user is allowed to archive the comments let mut repo_err = None; let connection = connections.exclusive()?; Ok(connection .transaction::<_, diesel::result::Error, _>(|| { - usecases::archive_comments(&*connection, ids).map_err(|err| { + usecases::archive_comments(&*connection, account_email, ids).map_err(|err| { warn!("Failed to archive {} comments: {}", ids.len(), err); repo_err = Some(err); diesel::result::Error::RollbackTransaction @@ -26,14 +30,27 @@ pub fn archive_comments(connections: &sqlite::Connections, ids: &[&str]) -> Resu mod tests { use super::super::tests::prelude::*; - fn archive_comments(fixture: &EnvFixture, ids: &[&str]) -> super::Result<()> { - super::archive_comments(&fixture.db_connections, ids) + fn archive_comments( + fixture: &EnvFixture, + account_email: &str, + ids: &[&str], + ) -> super::Result<()> { + super::archive_comments(&fixture.db_connections, account_email, ids) } #[test] fn should_archive_multiple_comments_only_once() { let fixture = EnvFixture::new(); + fixture.create_user( + usecases::NewUser { + email: "scout@foo.tld".into(), + password: "123456".into(), + username: "foo".into(), + }, + Some(Role::Scout), + ); + let entry_ids = vec![ fixture.create_entry(0.into()), fixture.create_entry(1.into()), @@ -80,6 +97,7 @@ mod tests { // Archive comments 1 and 2 assert!(archive_comments( &fixture, + "scout@foo.tld", &vec![&*rating_comment_ids[1].1, &*rating_comment_ids[2].1] ) .is_ok()); @@ -101,6 +119,7 @@ mod tests { // Try to archive comments 0 and 1 (already archived) assert_not_found(archive_comments( &fixture, + "scout@foo.tld", &vec![&*rating_comment_ids[0].1, &*rating_comment_ids[1].1], )); @@ -119,6 +138,7 @@ mod tests { // Archive remaining comments assert!(archive_comments( &fixture, + "scout@foo.tld", &vec![&*rating_comment_ids[0].1, &*rating_comment_ids[3].1] ) .is_ok()); diff --git a/src/infrastructure/flows/archive_ratings.rs b/src/infrastructure/flows/archive_ratings.rs index 95045c8a..ad8ee98f 100644 --- a/src/infrastructure/flows/archive_ratings.rs +++ b/src/infrastructure/flows/archive_ratings.rs @@ -2,12 +2,17 @@ use super::*; use diesel::connection::Connection; -pub fn exec_archive_ratings(connections: &sqlite::Connections, ids: &[&str]) -> Result<()> { +pub fn exec_archive_ratings( + connections: &sqlite::Connections, + account_email: &str, + ids: &[&str], +) -> Result<()> { + //TODO: check if user is allowed to archive the ratings let mut repo_err = None; let connection = connections.exclusive()?; Ok(connection .transaction::<_, diesel::result::Error, _>(|| { - usecases::archive_ratings(&*connection, ids).map_err(|err| { + usecases::archive_ratings(&*connection, account_email, ids).map_err(|err| { warn!("Failed to archive {} ratings: {}", ids.len(), err); repo_err = Some(err); diesel::result::Error::RollbackTransaction @@ -71,9 +76,10 @@ pub fn post_archive_ratings( pub fn archive_ratings( connections: &sqlite::Connections, indexer: &mut EntryIndexer, + account_email: &str, ids: &[&str], ) -> Result<()> { - exec_archive_ratings(connections, ids)?; + exec_archive_ratings(connections, account_email, ids)?; post_archive_ratings(connections, indexer, ids)?; Ok(()) } @@ -86,6 +92,7 @@ mod tests { super::archive_ratings( &fixture.db_connections, &mut *fixture.search_engine.borrow_mut(), + "scout@foo.tld", ids, ) } @@ -94,6 +101,15 @@ mod tests { fn should_archive_multiple_ratings_only_once() { let fixture = EnvFixture::new(); + fixture.create_user( + usecases::NewUser { + email: "scout@foo.tld".into(), + password: "123456".into(), + username: "foo".into(), + }, + Some(Role::Scout), + ); + let entry_ids = vec![ fixture.create_entry(0.into()), fixture.create_entry(1.into()), diff --git a/src/infrastructure/flows/change_user_role.rs b/src/infrastructure/flows/change_user_role.rs new file mode 100644 index 00000000..0d9f4316 --- /dev/null +++ b/src/infrastructure/flows/change_user_role.rs @@ -0,0 +1,73 @@ +use super::*; +use diesel::connection::Connection; + +pub fn change_user_role( + connections: &sqlite::Connections, + account_email: &str, + user_email: &str, + role: Role, +) -> Result<()> { + let mut repo_err = None; + let connection = connections.exclusive()?; + Ok(connection + .transaction::<_, diesel::result::Error, _>(|| { + usecases::change_user_role(&*connection, account_email, user_email, role).map_err( + |err| { + warn!("Failed to chage role for email {}: {}", user_email, err); + repo_err = Some(err); + diesel::result::Error::RollbackTransaction + }, + ) + }) + .map_err(|err| { + if let Some(repo_err) = repo_err { + repo_err + } else { + RepoError::from(err).into() + } + })?) +} + +#[cfg(test)] +mod tests { + use super::super::tests::prelude::*; + + fn change_user_role( + fixture: &EnvFixture, + account_email: &str, + user_email: &str, + role: Role, + ) -> super::Result<()> { + super::change_user_role(&fixture.db_connections, account_email, user_email, role) + } + + #[test] + fn should_change_the_role_to_scout_if_its_done_by_an_admin() { + let fixture = EnvFixture::new(); + fixture.create_user( + usecases::NewUser { + email: "user@bar.tld".into(), + password: "123456".into(), + username: "user".into(), + }, + None, + ); + fixture.create_user( + usecases::NewUser { + email: "admin@foo.tld".into(), + password: "123456".into(), + username: "admin".into(), + }, + Some(Role::Admin), + ); + assert_eq!( + fixture.try_get_user("user@bar.tld").unwrap().role, + Role::Guest + ); + assert!(change_user_role(&fixture, "admin@foo.tld", "user@bar.tld", Role::Scout).is_ok()); + assert_eq!( + fixture.try_get_user("user@bar.tld").unwrap().role, + Role::Scout + ); + } +} diff --git a/src/infrastructure/flows/mod.rs b/src/infrastructure/flows/mod.rs index 6720c873..ac357775 100644 --- a/src/infrastructure/flows/mod.rs +++ b/src/infrastructure/flows/mod.rs @@ -2,6 +2,7 @@ mod archive_comments; mod archive_entries; mod archive_events; mod archive_ratings; +mod change_user_role; mod create_entry; mod create_rating; mod update_entry; @@ -9,14 +10,13 @@ mod update_entry; pub mod prelude { pub use super::{ archive_comments::*, archive_entries::*, archive_events::*, archive_ratings::*, - create_entry::*, create_rating::*, update_entry::*, + change_user_role::*, create_entry::*, create_rating::*, update_entry::*, }; } pub type Result = std::result::Result; pub(crate) use super::{db::sqlite, error, notify}; - pub(crate) use crate::core::{prelude::*, usecases}; #[cfg(test)] @@ -90,6 +90,34 @@ mod tests { .unwrap() } + pub fn create_user(self: &EnvFixture, new_user: usecases::NewUser, role: Option) { + let email = { + let mut db = self.db_connections.exclusive().unwrap(); + let email = new_user.email.clone(); + usecases::create_new_user(&mut *db, new_user).unwrap(); + email + }; + if let Some(role) = role { + let mut u = self.try_get_user(&email).unwrap(); + u.role = role; + let db = self.db_connections.exclusive().unwrap(); + db.update_user(&u).unwrap(); + } + } + + pub fn try_get_user(self: &EnvFixture, email: &str) -> Option { + match self + .db_connections + .shared() + .unwrap() + .get_users_by_email(email) + { + Ok(users) => users.first().cloned(), + Err(RepoError::NotFound) => None, + x => x.map(|_| None).unwrap(), + } + } + pub fn try_get_entry(self: &EnvFixture, id: &str) -> Option { match self.db_connections.shared().unwrap().get_entry(id) { Ok(entry) => Some(entry), diff --git a/src/ports/web/frontend/login.rs b/src/ports/web/frontend/login.rs index 358dc4f2..2c7bb77e 100644 --- a/src/ports/web/frontend/login.rs +++ b/src/ports/web/frontend/login.rs @@ -5,7 +5,6 @@ use crate::{ ports::web::sqlite::Connections, }; use maud::Markup; -use num_traits::ToPrimitive; use rocket::{ self, http::{Cookie, Cookies}, @@ -65,12 +64,8 @@ pub fn post_login( }; Err(Flash::error(Redirect::to(uri!(get_login)), msg)) } - Ok(role) => { + Ok(_) => { cookies.add_private(Cookie::new(COOKIE_EMAIL_KEY, credentials.email)); - cookies.add_private(Cookie::new( - super::super::guards::COOKIE_USER_ACCESS_LEVEL, - role.to_usize().unwrap().to_string(), - )); Ok(Redirect::to(uri!(super::get_index))) } } diff --git a/src/ports/web/frontend/main.css b/src/ports/web/frontend/main.css index 74d64f7a..2c456f31 100644 --- a/src/ports/web/frontend/main.css +++ b/src/ports/web/frontend/main.css @@ -53,6 +53,12 @@ header nav a:hover { color: #5f5; } +main { + max-width: 98%; + margin: auto; + margin-top: 1em; +} + .search h1 { text-align: center; } @@ -209,4 +215,4 @@ fieldset { padding-right:0.4em; border-radius: 0.4em; color: #333; -} +} \ No newline at end of file diff --git a/src/ports/web/frontend/mod.rs b/src/ports/web/frontend/mod.rs index f7ae6222..ee7bb90f 100644 --- a/src/ports/web/frontend/mod.rs +++ b/src/ports/web/frontend/mod.rs @@ -1,10 +1,15 @@ use crate::{ - core::{prelude::*, usecases}, + core::{ + error::{Error, ParameterError}, + prelude::*, + usecases, + }, infrastructure::{db::sqlite, error::*, flows::prelude::*}, ports::web::{api::events::EventQuery, guards::*, tantivy::SearchEngine}, }; use chrono::NaiveDateTime; use maud::Markup; +use num_traits::FromPrimitive; use rocket::{ self, http::RawStr, @@ -25,6 +30,15 @@ const MAIN_CSS: &str = include_str!("main.css"); type Result = std::result::Result; +fn check_role(db: &dyn Db, account: &Account, role: Role) -> Result { + if let Some(user) = db.get_users_by_email(account.email())?.first() { + if user.role == role { + return Ok(user.to_owned()); + } + } + return Err(Error::Parameter(ParameterError::Unauthorized).into()); +} + #[get("/")] pub fn get_index_user(account: Account) -> Markup { view::index(Some(&account.email())) @@ -47,6 +61,49 @@ pub fn get_search(search_engine: SearchEngine, q: &RawStr, limit: Option) Ok(view::search_results(None, &q, &entries)) } +#[get("/search-users?")] +pub fn get_search_users( + pool: sqlite::Connections, + email: &RawStr, + account: Account, +) -> Result { + let email = email.url_decode()?; + { + let db = pool.shared()?; + let admin = check_role(&*db, &account, Role::Admin)?; + let users = db.get_users_by_email(&email)?; + Ok(view::user_search_result(&admin.email, &users)) + } +} + +#[derive(FromForm)] +pub struct ChangeUserRoleAction { + email: String, + role: u8, +} + +#[post("/change-user-role", data = "")] +pub fn post_change_user_role( + db: sqlite::Connections, + account: Account, + data: Form, +) -> std::result::Result> { + let d = data.into_inner(); + match Role::from_u8(d.role) { + None => Err(Flash::error( + Redirect::to(uri!(get_search_users:d.email)), + "Failed to change user role: invalid role.", + )), + Some(role) => match change_user_role(&db, account.email(), &d.email, role) { + Err(_) => Err(Flash::error( + Redirect::to(uri!(get_search_users:d.email)), + "Failed to change user role.", + )), + Ok(_) => Ok(Redirect::to(uri!(get_search_users:d.email))), + }, + } +} + #[get("/map.js")] pub fn get_map_js() -> JavaScript<&'static str> { JavaScript(MAP_JS) @@ -58,31 +115,30 @@ pub fn get_main_css() -> Css<&'static str> { } #[get("/entries/")] -pub fn get_entry_admin(pool: sqlite::Connections, id: &RawStr, admin: Admin) -> Result { +pub fn get_entry( + pool: sqlite::Connections, + id: &RawStr, + account: Option, +) -> Result { //TODO: dry out - let (e, ratings) = { - let db = pool.shared()?; - let e = db.get_entry(id.as_str())?; - let ratings = db.load_ratings_of_entry(&e.id)?; - let ratings_with_comments = db.zip_ratings_with_comments(ratings)?; - (e, ratings_with_comments) - }; - Ok(view::entry( - Some(&admin.0), - (e, ratings, Role::Admin).into(), - )) -} - -#[get("/entries/", rank = 2)] -pub fn get_entry(pool: sqlite::Connections, id: &RawStr) -> Result { - let (e, ratings) = { + let (user, e, ratings): (Option, _, _) = { let db = pool.shared()?; let e = db.get_entry(id.as_str())?; let ratings = db.load_ratings_of_entry(&e.id)?; let ratings_with_comments = db.zip_ratings_with_comments(ratings)?; - (e, ratings_with_comments) + let user = if let Some(a) = account { + db.get_users_by_email(a.email())? + .first() + .map(|u| u.to_owned()) + } else { + None + }; + (user, e, ratings_with_comments) }; - Ok(view::entry(None, (e, ratings).into())) + Ok(match user { + Some(u) => view::entry(Some(&u.email), (e, ratings, u.role).into()), + None => view::entry(None, (e, ratings).into()), + }) } #[get("/events/")] @@ -124,22 +180,25 @@ pub fn get_events(db: sqlite::Connections, query: EventQuery) -> Result } #[get("/dashboard")] -pub fn get_dashboard(db: sqlite::Connections, admin: Admin) -> Result { - let data = { - let db = db.shared()?; - let tag_count = db.count_tags()?; - let entry_count = db.count_entries()?; - let user_count = db.count_users()?; - let event_count = db.count_events()?; - view::DashBoardPresenter { - email: &admin.0, - entry_count, - event_count, - tag_count, - user_count, +pub fn get_dashboard(db: sqlite::Connections, account: Account) -> Result { + let db = db.shared()?; + let tag_count = db.count_tags()?; + let entry_count = db.count_entries()?; + let user_count = db.count_users()?; + let event_count = db.count_events()?; + let users = db.get_users_by_email(account.email())?; + if let Some(user) = users.first().cloned() { + if user.role == Role::Admin { + return Ok(view::dashboard(view::DashBoardPresenter { + user, + entry_count, + event_count, + tag_count, + user_count, + })); } - }; - Ok(view::dashboard(Some(&admin.0), data)) + } + Err(Error::Parameter(ParameterError::Unauthorized).into()) } #[derive(FromForm)] @@ -150,14 +209,14 @@ pub struct ArchiveAction { #[post("/comments/actions/archive", data = "")] pub fn post_comments_archive( - _admin: Admin, + account: Account, db: sqlite::Connections, data: Form, ) -> std::result::Result> { //TODO: dry out let d = data.into_inner(); let ids: Vec<_> = d.ids.split(',').filter(|id| !id.is_empty()).collect(); - match archive_comments(&db, &ids) { + match archive_comments(&db, account.email(), &ids) { Err(_) => Err(Flash::error( Redirect::to(uri!(get_entry:d.entry_id)), "Failed to achive the comment.", @@ -168,14 +227,14 @@ pub fn post_comments_archive( #[post("/ratings/actions/archive", data = "")] pub fn post_ratings_archive( - _admin: Admin, + account: Account, db: sqlite::Connections, mut search_engine: SearchEngine, data: Form, ) -> std::result::Result> { let d = data.into_inner(); let ids: Vec<_> = d.ids.split(',').filter(|id| !id.is_empty()).collect(); - match archive_ratings(&db, &mut search_engine, &ids) { + match archive_ratings(&db, &mut search_engine, account.email(), &ids) { Err(_) => Err(Flash::error( Redirect::to(uri!(get_entry:d.entry_id)), "Failed to archive the rating.", @@ -192,13 +251,14 @@ pub fn routes() -> Vec { get_dashboard, get_search, get_entry, - get_entry_admin, get_events, get_event, get_main_css, get_map_js, + get_search_users, post_comments_archive, post_ratings_archive, + post_change_user_role, login::get_login, login::get_login_user, login::post_login, diff --git a/src/ports/web/frontend/view/dashboard.rs b/src/ports/web/frontend/view/dashboard.rs index b7637e57..7a4882cb 100644 --- a/src/ports/web/frontend/view/dashboard.rs +++ b/src/ports/web/frontend/view/dashboard.rs @@ -1,22 +1,23 @@ use super::page; +use crate::core::entities::*; use maud::{html, Markup}; -pub struct DashBoardPresenter<'a> { - pub email: &'a str, +pub struct DashBoardPresenter { + pub user: User, pub entry_count: usize, pub event_count: usize, pub tag_count: usize, pub user_count: usize, } -pub fn dashboard(email: Option<&str>, data: DashBoardPresenter) -> Markup { +pub fn dashboard(data: DashBoardPresenter) -> Markup { page( "Admin Dashboard", - email, + Some(&data.user.email), None, None, html! { - main { + main class="dashboard" { h3 { "Database Statistics" } table { tr { @@ -36,6 +37,8 @@ pub fn dashboard(email: Option<&str>, data: DashBoardPresenter) -> Markup { td {(data.tag_count)} } } + h3 { "User Management" } + (super::search_users_form()) } }, ) diff --git a/src/ports/web/frontend/view/mod.rs b/src/ports/web/frontend/view/mod.rs index 0cd35aaa..3eda4870 100644 --- a/src/ports/web/frontend/view/mod.rs +++ b/src/ports/web/frontend/view/mod.rs @@ -1,5 +1,6 @@ use crate::core::prelude::*; use maud::{html, Markup}; +use num_traits::ToPrimitive; use rocket::request::FlashMessage; const LEAFLET_CSS_URL: &str = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.4.0/leaflet.css"; @@ -407,3 +408,67 @@ pub fn register(flash: Option) -> Markup { }, ) } + +pub fn user_search_result(admin_email: &str, users: &[User]) -> Markup { + page( + "Users", + Some(admin_email), + None, + None, + html! { + main { + h3 { "Users" } + (search_users_form()) + @if users.is_empty() { + "No users were found :(" + } @else { + table { + thead { + tr { + th { "Username" } + th { "eMail" } + th { "eMail confirmed" } + th { "Role" } + th { "Modify role" } + } + } + tbody { + @for u in users { + tr { + td { (u.username) } + td { (u.email) } + td { (if u.email_confirmed{"yes"}else{"no"}) } + td { (format!("{:?}",u.role)) } + td { + @if u.email != admin_email { + form action="change-user-role" method="POST" { + select name = "role" required? { + option value="-1" {"-- please select --"} + option value=(Role::Guest.to_u8().unwrap()) { "Guest" } + option value=(Role::User.to_u8().unwrap()) { "User" } + option value=(Role::Scout.to_u8().unwrap()) { "Scout" } + } + input type="hidden" name="email" value=(u.email); + input type="submit" value="change"; + } + } + } + } + } + } + } + } + } + }, + ) +} + +pub fn search_users_form() -> Markup { + html! { + form action="search-users" method="GET" { + input type="email" name="email" placeholder="email address"; + br; + input type="submit" value="search"; + } + } +} diff --git a/src/ports/web/guards.rs b/src/ports/web/guards.rs index fc5b5b87..53ef5bfa 100644 --- a/src/ports/web/guards.rs +++ b/src/ports/web/guards.rs @@ -1,5 +1,3 @@ -use crate::core::entities::Role; -use num_traits::FromPrimitive; use rocket::{ self, http::Status, @@ -10,7 +8,6 @@ use rocket::{ pub const COOKIE_EMAIL_KEY: &str = "ofdb-user-email"; pub const COOKIE_USER_KEY: &str = "user_id"; -pub const COOKIE_USER_ACCESS_LEVEL: &str = "ofdb-user-access-level"; #[derive(Debug)] pub struct Bearer(pub String); @@ -75,35 +72,3 @@ impl<'a, 'r> FromRequest<'a, 'r> for Account { .or_forward(()) } } - -#[derive(Debug)] -pub struct Admin(pub String); - -impl<'a, 'r> FromRequest<'a, 'r> for Admin { - type Error = (); - - fn from_request(request: &'a Request<'r>) -> request::Outcome { - let user = request - .cookies() - .get_private(COOKIE_EMAIL_KEY) - .and_then(|cookie| cookie.value().parse().ok()) - .map(Account); - let role = request - .cookies() - .get_private(COOKIE_USER_ACCESS_LEVEL) - .and_then(|cookie| cookie.value().parse().ok()) - .and_then(Role::from_usize); - - match (user, role) { - (Some(user), Some(role)) => { - if role == Role::Admin { - Some(Admin(user.0)) - } else { - return Outcome::Failure((Status::Unauthorized, ())); - } - } - _ => None, - } - .or_forward(()) - } -}