diff --git a/Cargo.lock b/Cargo.lock index 4f2df05..5cbedca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,6 +828,7 @@ dependencies = [ "rocket_cors", "rocket_db_pools", "rocket_dyn_templates", + "roles", "settings", "totp", "url", @@ -2686,6 +2687,17 @@ dependencies = [ "uncased", ] +[[package]] +name = "roles" +version = "0.0.0" +dependencies = [ + "chrono", + "database", + "id", + "serde", + "thiserror", +] + [[package]] name = "rsa" version = "0.7.2" diff --git a/crates/database/migrations/20230506170211_roles.down.sql b/crates/database/migrations/20230506170211_roles.down.sql new file mode 100644 index 0000000..289fe8b --- /dev/null +++ b/crates/database/migrations/20230506170211_roles.down.sql @@ -0,0 +1 @@ +drop table if exists roles; diff --git a/crates/database/migrations/20230506170211_roles.up.sql b/crates/database/migrations/20230506170211_roles.up.sql new file mode 100644 index 0000000..0deba39 --- /dev/null +++ b/crates/database/migrations/20230506170211_roles.up.sql @@ -0,0 +1,7 @@ +create table if not exists roles +( + name TEXT not null primary key, + label TEXT not null, + created_at TEXT not null default CURRENT_TIMESTAMP, + is_archived INTEGER not null default 0 +); diff --git a/crates/database/queries/roles/get_all.sql b/crates/database/queries/roles/get_all.sql new file mode 100644 index 0000000..3bad7fb --- /dev/null +++ b/crates/database/queries/roles/get_all.sql @@ -0,0 +1,7 @@ +select name, + label, + created_at as "created_at: DateTime", + is_archived as "is_archived: bool" +from roles + +order by created_at desc diff --git a/crates/database/queries/roles/insert.sql b/crates/database/queries/roles/insert.sql new file mode 100644 index 0000000..3bac2ae --- /dev/null +++ b/crates/database/queries/roles/insert.sql @@ -0,0 +1,2 @@ +insert into roles (name, label) +values (?, ?) diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index 7c415d0..66e82fb 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -286,6 +286,16 @@ }, "query": "update refresh_tokens\n\nset used_at = CURRENT_TIMESTAMP\n\nwhere token is ?" }, + "44cc12d7659e618fcb44a82697d60da940bdab3c87690ee42569ae3da1a8e791": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "insert into roles (name, label)\nvalues (?, ?)\n" + }, "520fe30e21f6b6c4d9a47c457675eebd144cf020e9230d154e9e4d0c8d6e01ca": { "describe": { "columns": [], @@ -474,6 +484,42 @@ }, "query": "update totp_login_requests\n\nset used_at = CURRENT_TIMESTAMP\n\nwhere user is ?\n and used_at is null" }, + "5e148a43d9f64c73296b2b70deef07d2c863a93fff693aec1b47378c44bf77de": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "label", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "is_archived: bool", + "ordinal": 3, + "type_info": "Int64" + } + ], + "nullable": [ + false, + false, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select name,\n label,\n created_at as \"created_at: DateTime\",\n is_archived as \"is_archived: bool\"\nfrom roles\n\norder by created_at desc\n" + }, "5f946348ad62389fab3c97a1563d1592cbc5180abbba6d5abd44326bf0862669": { "describe": { "columns": [ diff --git a/crates/database/src/tables.rs b/crates/database/src/tables.rs index 2f8c2e6..eadb26a 100644 --- a/crates/database/src/tables.rs +++ b/crates/database/src/tables.rs @@ -2,6 +2,7 @@ mod apps; mod authorization_codes; mod keys; mod refresh_tokens; +mod roles; mod settings; mod totp_login_requests; mod users; @@ -10,6 +11,7 @@ pub use apps::Apps; pub use authorization_codes::AuthorizationCodes; pub use keys::Keys; pub use refresh_tokens::RefreshTokens; +pub use roles::Roles; pub use settings::Settings; pub use totp_login_requests::TotpLoginRequests; pub use users::Users; diff --git a/crates/database/src/tables/roles.rs b/crates/database/src/tables/roles.rs new file mode 100644 index 0000000..162b9f4 --- /dev/null +++ b/crates/database/src/tables/roles.rs @@ -0,0 +1,34 @@ +use crate::error::{handle_error, Error}; +use sqlx::sqlite::SqliteQueryResult; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{FromRow, SqliteExecutor}; + +#[derive(FromRow)] +pub struct Roles { + pub name: String, + pub label: String, + pub created_at: DateTime, + pub is_archived: bool, +} + +impl Roles { + pub async fn insert( + conn: impl SqliteExecutor<'_>, + name: &str, + label: &str, + ) -> Result, Error> { + let query: SqliteQueryResult = sqlx::query_file!("queries/roles/insert.sql", name, label) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } + + pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result, Error> { + sqlx::query_file_as!(Self, "queries/roles/get_all.sql") + .fetch_all(conn) + .await + .map_err(handle_error) + } +} diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index 9292d82..32e7805 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -31,3 +31,4 @@ authorization_codes = { path = "../authorization_codes" } refresh_tokens = { path = "../refresh_tokens" } email = { path = "../email" } totp = { path = "../totp" } +roles = { path = "../roles" } diff --git a/crates/ezidam/src/error/conversion.rs b/crates/ezidam/src/error/conversion.rs index 4d84d8f..7571c5b 100644 --- a/crates/ezidam/src/error/conversion.rs +++ b/crates/ezidam/src/error/conversion.rs @@ -93,6 +93,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: roles::Error) -> Self { + Error::internal_server_error(e) + } +} + // std Types impl From for Error { diff --git a/crates/ezidam/src/icons.rs b/crates/ezidam/src/icons.rs index 06f4098..68c23eb 100644 --- a/crates/ezidam/src/icons.rs +++ b/crates/ezidam/src/icons.rs @@ -48,7 +48,8 @@ impl Icon { "2fa-large", TwoFaLarge, r#""#, "check", Check, r#""#, "x", X, r#""#, - "progress", Progress, r#""# + "progress", Progress, r#""#, + "users-group", UsersGroup, r#""# } } @@ -78,6 +79,7 @@ pub fn icons_to_templates(tera: &mut Tera) { Icon::Check, Icon::X, Icon::Progress, + Icon::UsersGroup, ]; // For each icon, it will output: ("icons/name", "...") diff --git a/crates/ezidam/src/id.rs b/crates/ezidam/src/id.rs index 15d3be3..db1d740 100644 --- a/crates/ezidam/src/id.rs +++ b/crates/ezidam/src/id.rs @@ -1,4 +1,4 @@ -use id::{AppID, Error, UserID}; +use id::{AppID, Error, RoleID, UserID}; use rocket::http::impl_from_uri_param_identity; use rocket::http::uri::fmt::{Formatter, Path, UriDisplay}; use rocket::request::FromParam; @@ -48,3 +48,23 @@ impl UriDisplay for RocketAppID { } impl_from_uri_param_identity!([Path] RocketAppID); + +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "rocket::serde")] +pub struct RocketRoleID(pub RoleID); + +impl<'r> FromParam<'r> for RocketRoleID { + type Error = Error; + + fn from_param(param: &'r str) -> Result { + RoleID::from_str(param).map(Self) + } +} + +impl UriDisplay for RocketRoleID { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + UriDisplay::fmt(&self.0 .0, f) + } +} + +impl_from_uri_param_identity!([Path] RocketRoleID); diff --git a/crates/ezidam/src/menu/items/admin.rs b/crates/ezidam/src/menu/items/admin.rs index dc5f18f..086b755 100644 --- a/crates/ezidam/src/menu/items/admin.rs +++ b/crates/ezidam/src/menu/items/admin.rs @@ -8,6 +8,7 @@ pub enum AdminMenu { Dashboard, Apps, Users, + Roles, Settings, } @@ -18,6 +19,7 @@ impl AdminMenu { AdminMenu::Dashboard => "dashboard", AdminMenu::Apps => "apps", AdminMenu::Users => "users", + AdminMenu::Roles => "roles", AdminMenu::Settings => "settings", } } @@ -51,6 +53,13 @@ impl AdminMenu { icon: Icon::Users.svg, sub: None, }, + MainItem { + id: AdminMenu::Roles.id(), + label: "Roles", + link: uri!(routes::admin::roles::admin_roles_list).to_string(), + icon: Icon::UsersGroup.svg, + sub: None, + }, MainItem { id: AdminMenu::Settings.id(), label: "Server settings", diff --git a/crates/ezidam/src/page.rs b/crates/ezidam/src/page.rs index 0556071..71d531f 100644 --- a/crates/ezidam/src/page.rs +++ b/crates/ezidam/src/page.rs @@ -32,6 +32,7 @@ pub enum Page { AuthorizeTotp(AuthorizeTotp), AdminUsersView(AdminUsersView), AdminUsersNew(AdminUsersNew), + AdminRolesList(AdminRolesList), } impl Page { @@ -60,6 +61,7 @@ impl Page { Page::AuthorizeTotp(_) => "pages/oauth/totp", Page::AdminUsersView(_) => "pages/admin/users/view", Page::AdminUsersNew(_) => "pages/admin/users/new", + Page::AdminRolesList(_) => "pages/admin/roles/list", } } @@ -88,6 +90,7 @@ impl Page { Page::AuthorizeTotp(_) => "Verifying your account", Page::AdminUsersView(_) => "User info", Page::AdminUsersNew(_) => "New user", + Page::AdminRolesList(_) => "Roles", } } @@ -118,6 +121,7 @@ impl Page { Page::AuthorizeTotp(_) => None, Page::AdminUsersView(_) => Some(AdminMenu::Users.into()), Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()), + Page::AdminRolesList(_) => Some(AdminMenu::Roles.into()), } } @@ -146,6 +150,7 @@ impl Page { Page::AuthorizeTotp(totp) => Box::new(totp), Page::AdminUsersView(view) => Box::new(view), Page::AdminUsersNew(new) => Box::new(new), + Page::AdminRolesList(list) => Box::new(list), } } } diff --git a/crates/ezidam/src/routes/admin.rs b/crates/ezidam/src/routes/admin.rs index cd6590c..5d90582 100644 --- a/crates/ezidam/src/routes/admin.rs +++ b/crates/ezidam/src/routes/admin.rs @@ -1,4 +1,5 @@ use self::apps::*; +use self::roles::*; use self::settings::*; use self::users::*; use dashboard::*; @@ -6,6 +7,7 @@ use rocket::{routes, Route}; pub mod apps; pub mod dashboard; +pub mod roles; pub mod settings; pub mod users; @@ -35,6 +37,7 @@ pub fn routes() -> Vec { admin_users_totp_secret_disable, admin_users_totp_backup_delete, admin_users_info_update, + admin_roles_list, ] } @@ -42,6 +45,7 @@ pub mod content { use apps::App; use jwt::JwtClaims; use rocket::serde::Serialize; + use roles::Role; use users::User; #[derive(Serialize)] @@ -122,4 +126,12 @@ pub mod content { pub struct AdminUsersNew { pub user: JwtClaims, } + + #[derive(Serialize)] + #[serde(crate = "rocket::serde")] + #[derive(Clone)] + pub struct AdminRolesList { + pub user: JwtClaims, + pub roles: Vec, + } } diff --git a/crates/ezidam/src/routes/admin/roles.rs b/crates/ezidam/src/routes/admin/roles.rs new file mode 100644 index 0000000..70da22b --- /dev/null +++ b/crates/ezidam/src/routes/admin/roles.rs @@ -0,0 +1,25 @@ +use crate::routes::prelude::*; +use rocket::{get, post}; +use roles::Role; + +#[get("/admin/roles")] +pub async fn admin_roles_list( + mut db: Connection, + admin: JwtAdmin, + flash: Option>, +) -> Result