Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ enable_logs = true
enable_features = true
logs_channel_id = 14043597305699
features_channel_id = 14069404548593076
panel_super_admin_users = []
panel_super_admin_roles = []

[bot.mode]
type = "dual"
Expand Down
12 changes: 12 additions & 0 deletions migrations/20251128142427_panel_permissions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS panel_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_type TEXT NOT NULL,
subject_id TEXT NOT NULL,
permission TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at INTEGER NOT NULL,
UNIQUE(subject_type, subject_id, permission)
);

CREATE INDEX idx_panel_perms_subject ON panel_permissions(subject_type, subject_id);
CREATE INDEX idx_panel_perms_permission ON panel_permissions(permission);
52 changes: 52 additions & 0 deletions rustmail/src/api/handler/admin/members.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::prelude::types::*;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::{Deserialize, Serialize};
use serenity::all::GuildId;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Debug, Serialize, Deserialize)]
pub struct MemberInfo {
pub user_id: String,
pub username: String,
pub discriminator: String,
pub avatar: Option<String>,
pub roles: Vec<String>,
}

pub async fn handle_list_members(
State(bot_state): State<Arc<Mutex<BotState>>>,
) -> impl IntoResponse {
let (guild_id, bot_http) = {
let state_lock = bot_state.lock().await;
let config = match &state_lock.config {
Some(c) => c,
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Config not loaded"}))).into_response(),
};
let http = match &state_lock.bot_http {
Some(h) => h.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Bot not initialized"}))).into_response(),
};
(config.bot.get_staff_guild_id(), http)
};

let guild_id_obj = GuildId::new(guild_id);

let members = match guild_id_obj.members(bot_http, None, None).await {
Ok(m) => m,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": format!("Failed to fetch members: {}", e)}))).into_response(),
};

let member_infos: Vec<MemberInfo> = members.iter().map(|m| MemberInfo {
user_id: m.user.id.to_string(),
username: m.user.name.clone(),
discriminator: m.user.discriminator.map(|d| d.to_string()).unwrap_or_else(|| "0".to_string()),
avatar: m.user.avatar.as_ref().map(|a| a.to_string()),
roles: m.roles.iter().map(|r| r.to_string()).collect(),
}).collect();

(StatusCode::OK, Json(member_infos)).into_response()
}
7 changes: 7 additions & 0 deletions rustmail/src/api/handler/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod members;
mod permissions;
mod roles;

pub use members::*;
pub use permissions::*;
pub use roles::*;
130 changes: 130 additions & 0 deletions rustmail/src/api/handler/admin/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::prelude::api::*;
use crate::prelude::types::*;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use axum_extra::extract::CookieJar;
use chrono::Utc;
use rustmail_types::api::panel_permissions::*;
use sqlx::{Row, query};
use std::sync::Arc;
use tokio::sync::Mutex;

pub async fn handle_list_permissions(
State(bot_state): State<Arc<Mutex<BotState>>>,
) -> impl IntoResponse {
let db_pool = {
let state_lock = bot_state.lock().await;
match &state_lock.db_pool {
Some(pool) => pool.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database not initialized"}))).into_response(),
}
};

let rows = match query("SELECT * FROM panel_permissions ORDER BY granted_at DESC")
.fetch_all(&db_pool)
.await
{
Ok(r) => r,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": format!("Database error: {}", e)}))).into_response(),
};

let mut permissions = Vec::new();
for row in rows {
if let (Ok(id), Ok(subject_type), Ok(subject_id), Ok(permission), Ok(granted_by), Ok(granted_at)) = (
row.try_get::<i64, _>("id"),
row.try_get::<String, _>("subject_type"),
row.try_get::<String, _>("subject_id"),
row.try_get::<String, _>("permission"),
row.try_get::<String, _>("granted_by"),
row.try_get::<i64, _>("granted_at"),
) {
if let (Some(st), Some(perm)) = (
SubjectType::from_str(&subject_type),
PanelPermission::from_str(&permission),
) {
permissions.push(PanelPermissionEntry {
id,
subject_type: st,
subject_id,
permission: perm,
granted_by,
granted_at,
});
}
}
}

(StatusCode::OK, Json(permissions)).into_response()
}

pub async fn handle_grant_permission(
State(bot_state): State<Arc<Mutex<BotState>>>,
jar: CookieJar,
Json(request): Json<GrantPermissionRequest>,
) -> impl IntoResponse {
let (db_pool, user_id) = {
let state_lock = bot_state.lock().await;
let pool = match &state_lock.db_pool {
Some(p) => p.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database not initialized"}))).into_response(),
};

let session_cookie = jar.get("session_id");
if session_cookie.is_none() {
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({"error": "Unauthorized"}))).into_response();
}

let session_id = session_cookie.unwrap().value().to_string();
let uid = get_user_id_from_session(&session_id, &pool).await;
(pool, uid)
};

let subject_type_str = request.subject_type.as_str();
let permission_str = request.permission.as_str();
let now = Utc::now().timestamp();

let result = query(
"INSERT INTO panel_permissions (subject_type, subject_id, permission, granted_by, granted_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(subject_type, subject_id, permission) DO UPDATE SET granted_by = ?, granted_at = ?"
)
.bind(subject_type_str)
.bind(&request.subject_id)
.bind(permission_str)
.bind(&user_id)
.bind(now)
.bind(&user_id)
.bind(now)
.execute(&db_pool)
.await;

match result {
Ok(_) => (StatusCode::OK, Json(serde_json::json!({"success": true}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": format!("Database error: {}", e)}))).into_response(),
}
}

pub async fn handle_revoke_permission(
State(bot_state): State<Arc<Mutex<BotState>>>,
Path(permission_id): Path<i64>,
) -> impl IntoResponse {
let db_pool = {
let state_lock = bot_state.lock().await;
match &state_lock.db_pool {
Some(pool) => pool.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database not initialized"}))).into_response(),
}
};

let result = query("DELETE FROM panel_permissions WHERE id = ?")
.bind(permission_id)
.execute(&db_pool)
.await;

match result {
Ok(_) => (StatusCode::OK, Json(serde_json::json!({"success": true}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": format!("Database error: {}", e)}))).into_response(),
}
}
52 changes: 52 additions & 0 deletions rustmail/src/api/handler/admin/roles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::prelude::types::*;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::{Deserialize, Serialize};
use serenity::all::GuildId;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Debug, Serialize, Deserialize)]
pub struct RoleInfo {
pub role_id: String,
pub name: String,
pub color: u32,
pub position: u16,
}

pub async fn handle_list_roles(
State(bot_state): State<Arc<Mutex<BotState>>>,
) -> impl IntoResponse {
let (guild_id, bot_http) = {
let state_lock = bot_state.lock().await;
let config = match &state_lock.config {
Some(c) => c,
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Config not loaded"}))).into_response(),
};
let http = match &state_lock.bot_http {
Some(h) => h.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Bot not initialized"}))).into_response(),
};
(config.bot.get_staff_guild_id(), http)
};

let guild_id_obj = GuildId::new(guild_id);

let roles = match guild_id_obj.roles(bot_http).await {
Ok(r) => r,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": format!("Failed to fetch roles: {}", e)}))).into_response(),
};

let mut role_infos: Vec<RoleInfo> = roles.iter().map(|(id, role)| RoleInfo {
role_id: id.to_string(),
name: role.name.clone(),
color: role.colour.0,
position: role.position,
}).collect();

role_infos.sort_by(|a, b| b.position.cmp(&a.position));

(StatusCode::OK, Json(role_infos)).into_response()
}
2 changes: 2 additions & 0 deletions rustmail/src/api/handler/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub mod admin;
pub mod apikeys;
pub mod auth;
pub mod bot;
pub mod externals;
pub mod panel;
pub mod user;

pub use admin::*;
pub use apikeys::*;
pub use auth::*;
pub use bot::*;
Expand Down
2 changes: 2 additions & 0 deletions rustmail/src/api/handler/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod avatar;
pub mod permissions;

pub use avatar::*;
pub use permissions::*;
49 changes: 49 additions & 0 deletions rustmail/src/api/handler/user/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::api::utils::panel_permissions::get_user_panel_permissions;
use crate::prelude::api::*;
use crate::prelude::types::*;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use axum_extra::extract::CookieJar;
use std::sync::Arc;
use tokio::sync::Mutex;

pub async fn handle_get_user_permissions(
State(bot_state): State<Arc<Mutex<BotState>>>,
jar: CookieJar,
) -> impl IntoResponse {
let (db_pool, config, guild_id, bot_http, user_id) = {
let state_lock = bot_state.lock().await;

let pool = match &state_lock.db_pool {
Some(p) => p.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database not initialized"}))).into_response(),
};

let cfg = match &state_lock.config {
Some(c) => c.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Config not loaded"}))).into_response(),
};

let http = match &state_lock.bot_http {
Some(h) => h.clone(),
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Bot not initialized"}))).into_response(),
};

let session_cookie = jar.get("session_id");
if session_cookie.is_none() {
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({"error": "Unauthorized"}))).into_response();
}

let session_id = session_cookie.unwrap().value().to_string();
let uid = get_user_id_from_session(&session_id, &pool).await;
let gid = cfg.bot.get_staff_guild_id();

(pool, cfg, gid, http, uid)
};

let permissions = get_user_panel_permissions(&user_id, &config, guild_id, bot_http, &db_pool).await;

(StatusCode::OK, Json(permissions)).into_response()
}
2 changes: 2 additions & 0 deletions rustmail/src/api/middleware/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod auth;
pub mod panel_permission;
pub mod permissions;

pub use auth::*;
pub use panel_permission::*;
pub use permissions::*;
Loading