Skip to content

Commit 5ca9f9a

Browse files
committed
feat(panel): add all permissions to admin section on panel
1 parent aa17f36 commit 5ca9f9a

8 files changed

Lines changed: 850 additions & 85 deletions

File tree

rustmail/src/api/utils/panel_permissions.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ use serenity::all::{GuildId, Http, UserId};
44
use sqlx::{Pool, Row, Sqlite, query};
55
use std::sync::Arc;
66

7-
pub fn get_panel_permissions_cache(
8-
) -> Arc<moka::future::Cache<String, Vec<PanelPermission>>> {
7+
pub fn get_panel_permissions_cache() -> Arc<moka::future::Cache<String, Vec<PanelPermission>>> {
98
use std::sync::OnceLock;
109
static CACHE: OnceLock<Arc<moka::future::Cache<String, Vec<PanelPermission>>>> =
1110
OnceLock::new();
@@ -41,11 +40,7 @@ pub async fn is_super_admin(
4140

4241
if let Ok(member) = guild_id_obj.member(bot_http, user_id_obj).await {
4342
for role_id in &member.roles {
44-
if config
45-
.bot
46-
.panel_super_admin_roles
47-
.contains(&role_id.get())
48-
{
43+
if config.bot.panel_super_admin_roles.contains(&role_id.get()) {
4944
return true;
5045
}
5146
}
@@ -78,9 +73,7 @@ pub async fn get_user_panel_permissions(
7873
PanelPermission::ManageApiKeys,
7974
PanelPermission::ManagePermissions,
8075
];
81-
cache
82-
.insert(user_id.to_string(), permissions.clone())
83-
.await;
76+
cache.insert(user_id.to_string(), permissions.clone()).await;
8477
return permissions;
8578
}
8679

@@ -90,6 +83,8 @@ pub async fn get_user_panel_permissions(
9083
PanelPermission::ManageBot,
9184
PanelPermission::ManageConfig,
9285
PanelPermission::ManageTickets,
86+
PanelPermission::ManageApiKeys,
87+
PanelPermission::ManagePermissions,
9388
];
9489
}
9590

@@ -114,16 +109,33 @@ pub async fn get_user_panel_permissions(
114109
let user_id_num = match user_id.parse::<u64>() {
115110
Ok(id) => id,
116111
Err(_) => {
117-
cache
118-
.insert(user_id.to_string(), permissions.clone())
119-
.await;
112+
cache.insert(user_id.to_string(), permissions.clone()).await;
120113
return permissions;
121114
}
122115
};
123116

124117
let guild_id_obj = GuildId::new(guild_id);
125118
let user_id_obj = UserId::new(user_id_num);
126119

120+
let everyone_role_id = guild_id.to_string();
121+
if let Ok(rows) = query(
122+
"SELECT permission FROM panel_permissions WHERE subject_type = 'role' AND subject_id = ?",
123+
)
124+
.bind(&everyone_role_id)
125+
.fetch_all(db_pool)
126+
.await
127+
{
128+
for row in rows {
129+
if let Ok(perm_str) = row.try_get::<String, _>("permission") {
130+
if let Some(perm) = PanelPermission::from_str(&perm_str) {
131+
if !permissions.contains(&perm) {
132+
permissions.push(perm);
133+
}
134+
}
135+
}
136+
}
137+
}
138+
127139
if let Ok(member) = guild_id_obj.member(bot_http, user_id_obj).await {
128140
for role_id in &member.roles {
129141
let role_id_str = role_id.get().to_string();
@@ -147,9 +159,7 @@ pub async fn get_user_panel_permissions(
147159
}
148160
}
149161

150-
cache
151-
.insert(user_id.to_string(), permissions.clone())
152-
.await;
162+
cache.insert(user_id.to_string(), permissions.clone()).await;
153163
permissions
154164
}
155165

rustmail_panel/src/components/api_keys.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::components::forbidden::Forbidden403;
2+
use crate::types::PanelPermission;
13
use gloo_net::http::Request;
24
use serde::{Deserialize, Serialize};
35
use wasm_bindgen_futures::spawn_local;
@@ -79,6 +81,35 @@ pub fn api_keys_page() -> Html {
7981
let show_create_modal = use_state(|| false);
8082
let created_key = use_state(|| None::<String>);
8183

84+
let permissions = use_state(|| None::<Vec<PanelPermission>>);
85+
{
86+
let permissions = permissions.clone();
87+
use_effect_with((), move |_| {
88+
spawn_local(async move {
89+
if let Ok(resp) = Request::get("/api/user/permissions").send().await {
90+
if let Ok(perms) = resp.json::<Vec<PanelPermission>>().await {
91+
permissions.set(Some(perms));
92+
}
93+
}
94+
});
95+
|| ()
96+
});
97+
}
98+
99+
if let Some(perms) = (*permissions).as_ref() {
100+
if !perms.contains(&PanelPermission::ManageApiKeys) {
101+
return html! {
102+
<Forbidden403 required_permission="Gérer les clés API" />
103+
};
104+
}
105+
} else {
106+
return html! {
107+
<div class="flex items-center justify-center min-h-[70vh]">
108+
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
109+
</div>
110+
};
111+
}
112+
82113
{
83114
let api_keys = api_keys.clone();
84115
let loading = loading.clone();

rustmail_panel/src/components/configuration.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::components::forbidden::Forbidden403;
2+
use crate::types::PanelPermission;
13
use gloo_net::http::Request;
24
use i18nrs::yew::use_translation;
35
use wasm_bindgen_futures::spawn_local;
@@ -18,6 +20,35 @@ pub fn configuration_page() -> Html {
1820

1921
let expanded_sections = use_state(|| vec![true, false, false, false, false, false, false, false]);
2022

23+
let permissions = use_state(|| None::<Vec<PanelPermission>>);
24+
{
25+
let permissions = permissions.clone();
26+
use_effect_with((), move |_| {
27+
spawn_local(async move {
28+
if let Ok(resp) = Request::get("/api/user/permissions").send().await {
29+
if let Ok(perms) = resp.json::<Vec<PanelPermission>>().await {
30+
permissions.set(Some(perms));
31+
}
32+
}
33+
});
34+
|| ()
35+
});
36+
}
37+
38+
if let Some(perms) = (*permissions).as_ref() {
39+
if !perms.contains(&PanelPermission::ManageConfig) {
40+
return html! {
41+
<Forbidden403 required_permission="Gérer la configuration" />
42+
};
43+
}
44+
} else {
45+
return html! {
46+
<div class="flex items-center justify-center min-h-[70vh]">
47+
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
48+
</div>
49+
};
50+
}
51+
2152
{
2253
let bot_status = bot_status.clone();
2354
let is_loading = is_loading.clone();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::pages::panel::PanelRoute;
2+
use yew::prelude::*;
3+
use yew_router::prelude::*;
4+
5+
#[derive(Properties, PartialEq)]
6+
pub struct Forbidden403Props {
7+
pub required_permission: String,
8+
}
9+
10+
#[function_component(Forbidden403)]
11+
pub fn forbidden_403(props: &Forbidden403Props) -> Html {
12+
let navigator = use_navigator().unwrap();
13+
14+
let go_home = {
15+
let navigator = navigator.clone();
16+
Callback::from(move |_| {
17+
navigator.push(&PanelRoute::Home);
18+
})
19+
};
20+
21+
html! {
22+
<div class="flex flex-col items-center justify-center min-h-[calc(100vh-12rem)] px-4">
23+
<div class="max-w-md w-full bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl shadow-2xl p-8 border border-slate-700 text-center">
24+
<div class="flex justify-center mb-6">
25+
<div class="bg-red-500/10 p-4 rounded-full">
26+
<svg class="w-16 h-16 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
28+
</svg>
29+
</div>
30+
</div>
31+
32+
<h1 class="text-3xl font-bold text-white mb-3">{"Accès refusé"}</h1>
33+
34+
<p class="text-gray-400 mb-2">
35+
{"Vous n'avez pas la permission nécessaire pour accéder à cette page."}
36+
</p>
37+
38+
<div class="bg-slate-900/50 border border-slate-700 rounded-lg p-4 mb-6">
39+
<p class="text-sm text-gray-500 mb-1">{"Permission requise :"}</p>
40+
<p class="text-lg font-semibold text-red-400">{&props.required_permission}</p>
41+
</div>
42+
43+
<p class="text-sm text-gray-500 mb-6">
44+
{"Contactez un administrateur si vous pensez que vous devriez avoir accès à cette section."}
45+
</p>
46+
47+
<button
48+
onclick={go_home}
49+
class="w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center space-x-2"
50+
>
51+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
52+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
53+
</svg>
54+
<span>{"Retour à l'accueil"}</span>
55+
</button>
56+
</div>
57+
</div>
58+
}
59+
}

rustmail_panel/src/components/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod api_keys;
22
pub mod configuration;
3+
pub mod forbidden;
34
pub mod home;
45
pub mod language_switcher;
56
pub mod logout_button;

rustmail_panel/src/components/ticket.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::components::forbidden::Forbidden403;
2+
use crate::types::PanelPermission;
13
use crate::utils::markdown::markdown_to_html_safe;
24
use gloo_net::http::Request;
35
use i18nrs::yew::use_translation;
@@ -128,6 +130,35 @@ pub fn tickets_list() -> Html {
128130
let total_pages = use_state(|| 1i64);
129131
let total_tickets = use_state(|| 0i64);
130132

133+
let permissions = use_state(|| None::<Vec<PanelPermission>>);
134+
{
135+
let permissions = permissions.clone();
136+
use_effect_with((), move |_| {
137+
spawn_local(async move {
138+
if let Ok(resp) = Request::get("/api/user/permissions").send().await {
139+
if let Ok(perms) = resp.json::<Vec<PanelPermission>>().await {
140+
permissions.set(Some(perms));
141+
}
142+
}
143+
});
144+
|| ()
145+
});
146+
}
147+
148+
if let Some(perms) = (*permissions).as_ref() {
149+
if !perms.contains(&PanelPermission::ManageTickets) {
150+
return html! {
151+
<Forbidden403 required_permission="Gérer les tickets" />
152+
};
153+
}
154+
} else {
155+
return html! {
156+
<div class="flex items-center justify-center min-h-[70vh]">
157+
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
158+
</div>
159+
};
160+
}
161+
131162
{
132163
let current_page = current_page.clone();
133164
let page_size = page_size.clone();
@@ -473,6 +504,35 @@ pub fn ticket_details(props: &TicketDetailsProps) -> Html {
473504
let show_system = use_state(|| true);
474505
let search_query = use_state(|| String::new());
475506

507+
let permissions = use_state(|| None::<Vec<PanelPermission>>);
508+
{
509+
let permissions = permissions.clone();
510+
use_effect_with((), move |_| {
511+
spawn_local(async move {
512+
if let Ok(resp) = Request::get("/api/user/permissions").send().await {
513+
if let Ok(perms) = resp.json::<Vec<PanelPermission>>().await {
514+
permissions.set(Some(perms));
515+
}
516+
}
517+
});
518+
|| ()
519+
});
520+
}
521+
522+
if let Some(perms) = (*permissions).as_ref() {
523+
if !perms.contains(&PanelPermission::ManageTickets) {
524+
return html! {
525+
<Forbidden403 required_permission="Gérer les tickets" />
526+
};
527+
}
528+
} else {
529+
return html! {
530+
<div class="flex items-center justify-center min-h-[70vh]">
531+
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
532+
</div>
533+
};
534+
}
535+
476536
{
477537
let id = props.id.clone();
478538
let ticket = ticket.clone();

0 commit comments

Comments
 (0)