roles: sql, crate, struct, id, rocket id, list page
This commit is contained in:
parent
2e055e2037
commit
efaf2bda5a
23 changed files with 462 additions and 2 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -828,6 +828,7 @@ dependencies = [
|
||||||
"rocket_cors",
|
"rocket_cors",
|
||||||
"rocket_db_pools",
|
"rocket_db_pools",
|
||||||
"rocket_dyn_templates",
|
"rocket_dyn_templates",
|
||||||
|
"roles",
|
||||||
"settings",
|
"settings",
|
||||||
"totp",
|
"totp",
|
||||||
"url",
|
"url",
|
||||||
|
|
@ -2686,6 +2687,17 @@ dependencies = [
|
||||||
"uncased",
|
"uncased",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roles"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"database",
|
||||||
|
"id",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
|
||||||
1
crates/database/migrations/20230506170211_roles.down.sql
Normal file
1
crates/database/migrations/20230506170211_roles.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
drop table if exists roles;
|
||||||
7
crates/database/migrations/20230506170211_roles.up.sql
Normal file
7
crates/database/migrations/20230506170211_roles.up.sql
Normal file
|
|
@ -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
|
||||||
|
);
|
||||||
7
crates/database/queries/roles/get_all.sql
Normal file
7
crates/database/queries/roles/get_all.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
select name,
|
||||||
|
label,
|
||||||
|
created_at as "created_at: DateTime<Utc>",
|
||||||
|
is_archived as "is_archived: bool"
|
||||||
|
from roles
|
||||||
|
|
||||||
|
order by created_at desc
|
||||||
2
crates/database/queries/roles/insert.sql
Normal file
2
crates/database/queries/roles/insert.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
insert into roles (name, label)
|
||||||
|
values (?, ?)
|
||||||
|
|
@ -286,6 +286,16 @@
|
||||||
},
|
},
|
||||||
"query": "update refresh_tokens\n\nset used_at = CURRENT_TIMESTAMP\n\nwhere token is ?"
|
"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": {
|
"520fe30e21f6b6c4d9a47c457675eebd144cf020e9230d154e9e4d0c8d6e01ca": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"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"
|
"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<Utc>",
|
||||||
|
"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<Utc>\",\n is_archived as \"is_archived: bool\"\nfrom roles\n\norder by created_at desc\n"
|
||||||
|
},
|
||||||
"5f946348ad62389fab3c97a1563d1592cbc5180abbba6d5abd44326bf0862669": {
|
"5f946348ad62389fab3c97a1563d1592cbc5180abbba6d5abd44326bf0862669": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod apps;
|
||||||
mod authorization_codes;
|
mod authorization_codes;
|
||||||
mod keys;
|
mod keys;
|
||||||
mod refresh_tokens;
|
mod refresh_tokens;
|
||||||
|
mod roles;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod totp_login_requests;
|
mod totp_login_requests;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
@ -10,6 +11,7 @@ pub use apps::Apps;
|
||||||
pub use authorization_codes::AuthorizationCodes;
|
pub use authorization_codes::AuthorizationCodes;
|
||||||
pub use keys::Keys;
|
pub use keys::Keys;
|
||||||
pub use refresh_tokens::RefreshTokens;
|
pub use refresh_tokens::RefreshTokens;
|
||||||
|
pub use roles::Roles;
|
||||||
pub use settings::Settings;
|
pub use settings::Settings;
|
||||||
pub use totp_login_requests::TotpLoginRequests;
|
pub use totp_login_requests::TotpLoginRequests;
|
||||||
pub use users::Users;
|
pub use users::Users;
|
||||||
|
|
|
||||||
34
crates/database/src/tables/roles.rs
Normal file
34
crates/database/src/tables/roles.rs
Normal file
|
|
@ -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<Utc>,
|
||||||
|
pub is_archived: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Roles {
|
||||||
|
pub async fn insert(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
name: &str,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<Option<()>, 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<Vec<Self>, Error> {
|
||||||
|
sqlx::query_file_as!(Self, "queries/roles/get_all.sql")
|
||||||
|
.fetch_all(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,3 +31,4 @@ authorization_codes = { path = "../authorization_codes" }
|
||||||
refresh_tokens = { path = "../refresh_tokens" }
|
refresh_tokens = { path = "../refresh_tokens" }
|
||||||
email = { path = "../email" }
|
email = { path = "../email" }
|
||||||
totp = { path = "../totp" }
|
totp = { path = "../totp" }
|
||||||
|
roles = { path = "../roles" }
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,12 @@ impl From<totp::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<roles::Error> for Error {
|
||||||
|
fn from(e: roles::Error) -> Self {
|
||||||
|
Error::internal_server_error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// std Types
|
// std Types
|
||||||
|
|
||||||
impl From<String> for Error {
|
impl From<String> for Error {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ impl Icon {
|
||||||
"2fa-large", TwoFaLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-2fa" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path><path d="M10 16v-8h4"></path><path d="M10 12l3 0"></path><path d="M17 16v-6a2 2 0 0 1 4 0v6"></path><path d="M17 13l4 0"></path></svg>"#,
|
"2fa-large", TwoFaLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-2fa" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path><path d="M10 16v-8h4"></path><path d="M10 12l3 0"></path><path d="M17 16v-6a2 2 0 0 1 4 0v6"></path><path d="M17 13l4 0"></path></svg>"#,
|
||||||
"check", Check, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>"#,
|
"check", Check, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>"#,
|
||||||
"x", X, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg>"#,
|
"x", X, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg>"#,
|
||||||
"progress", Progress, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-progress" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 20.777a8.942 8.942 0 0 1 -2.48 -.969"></path><path d="M14 3.223a9.003 9.003 0 0 1 0 17.554"></path><path d="M4.579 17.093a8.961 8.961 0 0 1 -1.227 -2.592"></path><path d="M3.124 10.5c.16 -.95 .468 -1.85 .9 -2.675l.169 -.305"></path><path d="M6.907 4.579a8.954 8.954 0 0 1 3.093 -1.356"></path></svg>"#
|
"progress", Progress, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-progress" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 20.777a8.942 8.942 0 0 1 -2.48 -.969"></path><path d="M14 3.223a9.003 9.003 0 0 1 0 17.554"></path><path d="M4.579 17.093a8.961 8.961 0 0 1 -1.227 -2.592"></path><path d="M3.124 10.5c.16 -.95 .468 -1.85 .9 -2.675l.169 -.305"></path><path d="M6.907 4.579a8.954 8.954 0 0 1 3.093 -1.356"></path></svg>"#,
|
||||||
|
"users-group", UsersGroup, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +79,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
|
||||||
Icon::Check,
|
Icon::Check,
|
||||||
Icon::X,
|
Icon::X,
|
||||||
Icon::Progress,
|
Icon::Progress,
|
||||||
|
Icon::UsersGroup,
|
||||||
];
|
];
|
||||||
|
|
||||||
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
||||||
|
|
|
||||||
|
|
@ -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::impl_from_uri_param_identity;
|
||||||
use rocket::http::uri::fmt::{Formatter, Path, UriDisplay};
|
use rocket::http::uri::fmt::{Formatter, Path, UriDisplay};
|
||||||
use rocket::request::FromParam;
|
use rocket::request::FromParam;
|
||||||
|
|
@ -48,3 +48,23 @@ impl UriDisplay<Path> for RocketAppID {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from_uri_param_identity!([Path] 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<Self, Self::Error> {
|
||||||
|
RoleID::from_str(param).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UriDisplay<Path> for RocketRoleID {
|
||||||
|
fn fmt(&self, f: &mut Formatter<Path>) -> fmt::Result {
|
||||||
|
UriDisplay::fmt(&self.0 .0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from_uri_param_identity!([Path] RocketRoleID);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub enum AdminMenu {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
Apps,
|
Apps,
|
||||||
Users,
|
Users,
|
||||||
|
Roles,
|
||||||
Settings,
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ impl AdminMenu {
|
||||||
AdminMenu::Dashboard => "dashboard",
|
AdminMenu::Dashboard => "dashboard",
|
||||||
AdminMenu::Apps => "apps",
|
AdminMenu::Apps => "apps",
|
||||||
AdminMenu::Users => "users",
|
AdminMenu::Users => "users",
|
||||||
|
AdminMenu::Roles => "roles",
|
||||||
AdminMenu::Settings => "settings",
|
AdminMenu::Settings => "settings",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +53,13 @@ impl AdminMenu {
|
||||||
icon: Icon::Users.svg,
|
icon: Icon::Users.svg,
|
||||||
sub: None,
|
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 {
|
MainItem {
|
||||||
id: AdminMenu::Settings.id(),
|
id: AdminMenu::Settings.id(),
|
||||||
label: "Server settings",
|
label: "Server settings",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub enum Page {
|
||||||
AuthorizeTotp(AuthorizeTotp),
|
AuthorizeTotp(AuthorizeTotp),
|
||||||
AdminUsersView(AdminUsersView),
|
AdminUsersView(AdminUsersView),
|
||||||
AdminUsersNew(AdminUsersNew),
|
AdminUsersNew(AdminUsersNew),
|
||||||
|
AdminRolesList(AdminRolesList),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -60,6 +61,7 @@ impl Page {
|
||||||
Page::AuthorizeTotp(_) => "pages/oauth/totp",
|
Page::AuthorizeTotp(_) => "pages/oauth/totp",
|
||||||
Page::AdminUsersView(_) => "pages/admin/users/view",
|
Page::AdminUsersView(_) => "pages/admin/users/view",
|
||||||
Page::AdminUsersNew(_) => "pages/admin/users/new",
|
Page::AdminUsersNew(_) => "pages/admin/users/new",
|
||||||
|
Page::AdminRolesList(_) => "pages/admin/roles/list",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +90,7 @@ impl Page {
|
||||||
Page::AuthorizeTotp(_) => "Verifying your account",
|
Page::AuthorizeTotp(_) => "Verifying your account",
|
||||||
Page::AdminUsersView(_) => "User info",
|
Page::AdminUsersView(_) => "User info",
|
||||||
Page::AdminUsersNew(_) => "New user",
|
Page::AdminUsersNew(_) => "New user",
|
||||||
|
Page::AdminRolesList(_) => "Roles",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +121,7 @@ impl Page {
|
||||||
Page::AuthorizeTotp(_) => None,
|
Page::AuthorizeTotp(_) => None,
|
||||||
Page::AdminUsersView(_) => Some(AdminMenu::Users.into()),
|
Page::AdminUsersView(_) => Some(AdminMenu::Users.into()),
|
||||||
Page::AdminUsersNew(_) => 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::AuthorizeTotp(totp) => Box::new(totp),
|
||||||
Page::AdminUsersView(view) => Box::new(view),
|
Page::AdminUsersView(view) => Box::new(view),
|
||||||
Page::AdminUsersNew(new) => Box::new(new),
|
Page::AdminUsersNew(new) => Box::new(new),
|
||||||
|
Page::AdminRolesList(list) => Box::new(list),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use self::apps::*;
|
use self::apps::*;
|
||||||
|
use self::roles::*;
|
||||||
use self::settings::*;
|
use self::settings::*;
|
||||||
use self::users::*;
|
use self::users::*;
|
||||||
use dashboard::*;
|
use dashboard::*;
|
||||||
|
|
@ -6,6 +7,7 @@ use rocket::{routes, Route};
|
||||||
|
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
|
pub mod roles;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
|
|
@ -35,6 +37,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
admin_users_totp_secret_disable,
|
admin_users_totp_secret_disable,
|
||||||
admin_users_totp_backup_delete,
|
admin_users_totp_backup_delete,
|
||||||
admin_users_info_update,
|
admin_users_info_update,
|
||||||
|
admin_roles_list,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,6 +45,7 @@ pub mod content {
|
||||||
use apps::App;
|
use apps::App;
|
||||||
use jwt::JwtClaims;
|
use jwt::JwtClaims;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
|
use roles::Role;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -122,4 +126,12 @@ pub mod content {
|
||||||
pub struct AdminUsersNew {
|
pub struct AdminUsersNew {
|
||||||
pub user: JwtClaims,
|
pub user: JwtClaims,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminRolesList {
|
||||||
|
pub user: JwtClaims,
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
crates/ezidam/src/routes/admin/roles.rs
Normal file
25
crates/ezidam/src/routes/admin/roles.rs
Normal file
|
|
@ -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<Database>,
|
||||||
|
admin: JwtAdmin,
|
||||||
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
) -> Result<Template> {
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
let roles = Role::get_all(&mut transaction).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let page = Page::AdminRolesList(super::content::AdminRolesList {
|
||||||
|
user: admin.0,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
}
|
||||||
127
crates/ezidam/templates/pages/admin/roles/list.html.tera
Normal file
127
crates/ezidam/templates/pages/admin/roles/list.html.tera
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
{% extends "shell" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% set new_role_label = "New role" %}
|
||||||
|
{% set new_role_link = "roles/new" %}
|
||||||
|
<div class="page-header d-print-none">
|
||||||
|
<div class="container-xl">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
{% if roles | length != 0 %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="page-pretitle">
|
||||||
|
Admin dashboard
|
||||||
|
</div>
|
||||||
|
<h2 class="page-title">
|
||||||
|
Roles
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<div class="btn-list">
|
||||||
|
<a href="{{ new_role_link }}" class="btn btn-primary d-none d-sm-inline-block">
|
||||||
|
{% include "icons/plus" %}
|
||||||
|
{{ new_role_label }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ new_role_link }}" class="btn btn-primary d-sm-none btn-icon"
|
||||||
|
aria-label="{{ new_role_label }}">
|
||||||
|
{% include "icons/plus" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Page body -->
|
||||||
|
<div class="page-body">
|
||||||
|
<div class="container-xl">
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
<div class="alert alert-{{flash.0}}" role="alert">
|
||||||
|
<h4 class="alert-title">{{ flash.1 }}</h4>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if roles | length != 0 %}
|
||||||
|
<div class="card">
|
||||||
|
<div id="table-default" class="table-responsive">
|
||||||
|
<table class="table table-hover table-vcenter">
|
||||||
|
<!-- Table header -->
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<button class="table-sort" data-sort="sort-name">Name</button>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<button class="table-sort" data-sort="sort-status">Status</button>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<button class="table-sort" data-sort="sort-creation-date">Creation Date</button>
|
||||||
|
</th>
|
||||||
|
<th class="w-1">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<!-- Table content -->
|
||||||
|
<tbody class="table-tbody">
|
||||||
|
{% for role in roles %}
|
||||||
|
<tr>
|
||||||
|
<td class="sort-name">{{ role.label }}</td>
|
||||||
|
<td class="sort-status text-nowrap">
|
||||||
|
{% if role.is_archived == true %}
|
||||||
|
<span class="badge bg-danger me-1"></span> Archived
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success me-1"></span> Active
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="sort-creation-date" data-date='{{ role.created_at | date(format="%s") }}'>
|
||||||
|
{{ role.created_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="roles/{{ role.name }}">Details</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="d-flex flex-column justify-content-center">
|
||||||
|
<div class="empty">
|
||||||
|
<p class="empty-title">No roles</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
Create a role to limit users in applications outside ezidam.
|
||||||
|
</p>
|
||||||
|
<div class="empty-action">
|
||||||
|
<a href="{{ new_role_link }}" class="btn btn-primary">
|
||||||
|
{% include "icons/plus" %}
|
||||||
|
{{ new_role_label }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block libs_js %}
|
||||||
|
<script src="/libs/list.js/list.min.js" defer></script>
|
||||||
|
{% endblock lib_js %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const list = new List('table-default', {
|
||||||
|
sortClass: 'table-sort',
|
||||||
|
listClass: 'table-tbody',
|
||||||
|
valueNames: ['sort-name', 'sort-status',
|
||||||
|
{attr: 'data-date', name: 'sort-creation-date'},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock additional_js %}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod app;
|
mod app;
|
||||||
mod key;
|
mod key;
|
||||||
|
mod role;
|
||||||
mod user;
|
mod user;
|
||||||
mod username;
|
mod username;
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ pub enum Error {
|
||||||
|
|
||||||
pub use app::AppID;
|
pub use app::AppID;
|
||||||
pub use key::KeyID;
|
pub use key::KeyID;
|
||||||
|
pub use role::RoleID;
|
||||||
pub use user::UserID;
|
pub use user::UserID;
|
||||||
pub use username::Username;
|
pub use username::Username;
|
||||||
pub use username::INVALID_USERNAME_ERROR;
|
pub use username::INVALID_USERNAME_ERROR;
|
||||||
|
|
|
||||||
63
crates/id/src/role.rs
Normal file
63
crates/id/src/role.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use super::Error;
|
||||||
|
use nanoid_dictionary::{LOWERCASE, NUMBERS};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
pub struct RoleID(pub String);
|
||||||
|
|
||||||
|
impl Display for RoleID {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for RoleID {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&RoleID> for String {
|
||||||
|
fn from(value: &RoleID) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RoleID {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.chars()
|
||||||
|
.all(|c| LOWERCASE.contains(&c) || NUMBERS.contains(&c) || c == '-')
|
||||||
|
{
|
||||||
|
Ok(Self(s.to_string()))
|
||||||
|
} else {
|
||||||
|
Err(Error::Invalid("Role"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::RoleID;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_characters() {
|
||||||
|
let value = "AER Rennes tek5";
|
||||||
|
|
||||||
|
assert!(RoleID::from_str(value).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_role() {
|
||||||
|
let value = "aer-rennes-tek5";
|
||||||
|
let id = RoleID::from_str(value);
|
||||||
|
assert!(id.is_ok());
|
||||||
|
|
||||||
|
let id = id.unwrap();
|
||||||
|
assert_eq!(id.as_ref(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/roles/Cargo.toml
Normal file
11
crates/roles/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "roles"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
database = { path = "../database" }
|
||||||
|
id = { path = "../id" }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
26
crates/roles/src/database.rs
Normal file
26
crates/roles/src/database.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::Role;
|
||||||
|
use database::sqlx::SqliteExecutor;
|
||||||
|
use database::Roles as DatabaseRoles;
|
||||||
|
use id::RoleID;
|
||||||
|
|
||||||
|
impl From<DatabaseRoles> for Role {
|
||||||
|
fn from(db: DatabaseRoles) -> Self {
|
||||||
|
Self {
|
||||||
|
name: RoleID(db.name),
|
||||||
|
label: db.label,
|
||||||
|
created_at: db.created_at,
|
||||||
|
is_archived: db.is_archived,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Role {
|
||||||
|
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
||||||
|
Ok(DatabaseRoles::get_all(conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(Self::from)
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/roles/src/error.rs
Normal file
8
crates/roles/src/error.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// error
|
||||||
|
#[derive(thiserror::Error)]
|
||||||
|
// the rest
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Database: {0}")]
|
||||||
|
Database(#[from] database::Error),
|
||||||
|
}
|
||||||
32
crates/roles/src/lib.rs
Normal file
32
crates/roles/src/lib.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
mod database;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use id::RoleID;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
// Exports
|
||||||
|
pub use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct Role {
|
||||||
|
name: RoleID,
|
||||||
|
label: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
is_archived: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Role {
|
||||||
|
pub fn name(&self) -> &RoleID {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
pub fn label(&self) -> &str {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
pub fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
pub fn is_archived(&self) -> bool {
|
||||||
|
self.is_archived
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue