Skip to content

Commit 6b67b19

Browse files
committed
feat(api): add categories management endpoints
1 parent a972f8c commit 6b67b19

8 files changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use crate::db::operations::ticket_categories::CATEGORY_BUTTON_HARD_LIMIT;
2+
use crate::db::operations::{
3+
count_enabled_categories, create_category, delete_category, get_category_by_id,
4+
get_category_by_name, get_category_settings, list_all_categories, update_category,
5+
update_category_settings,
6+
};
7+
use crate::db::repr::{TicketCategory, TicketCategorySettings};
8+
use crate::prelude::types::*;
9+
use axum::Json;
10+
use axum::extract::{Path, State};
11+
use axum::http::StatusCode;
12+
use serde::{Deserialize, Serialize};
13+
use sqlx::SqlitePool;
14+
use std::sync::Arc;
15+
use tokio::sync::Mutex;
16+
17+
async fn pool(bot_state: &Arc<Mutex<BotState>>) -> Result<SqlitePool, (StatusCode, String)> {
18+
let state_lock = bot_state.lock().await;
19+
match &state_lock.db_pool {
20+
Some(p) => Ok(p.clone()),
21+
None => Err((
22+
StatusCode::INTERNAL_SERVER_ERROR,
23+
"Database not initialized".to_string(),
24+
)),
25+
}
26+
}
27+
28+
fn internal(e: impl ToString) -> (StatusCode, String) {
29+
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
30+
}
31+
32+
#[derive(Serialize, Deserialize)]
33+
pub struct CategoryDto {
34+
pub id: String,
35+
pub name: String,
36+
pub description: Option<String>,
37+
pub emoji: Option<String>,
38+
pub discord_category_id: String,
39+
pub position: i64,
40+
pub enabled: bool,
41+
pub created_at: i64,
42+
pub updated_at: i64,
43+
}
44+
45+
impl From<TicketCategory> for CategoryDto {
46+
fn from(c: TicketCategory) -> Self {
47+
Self {
48+
id: c.id,
49+
name: c.name,
50+
description: c.description,
51+
emoji: c.emoji,
52+
discord_category_id: c.discord_category_id,
53+
position: c.position,
54+
enabled: c.enabled,
55+
created_at: c.created_at,
56+
updated_at: c.updated_at,
57+
}
58+
}
59+
}
60+
61+
pub async fn list_categories_handler(
62+
State(bot_state): State<Arc<Mutex<BotState>>>,
63+
) -> Result<Json<Vec<CategoryDto>>, (StatusCode, String)> {
64+
let p = pool(&bot_state).await?;
65+
let cats = list_all_categories(&p).await.map_err(internal)?;
66+
Ok(Json(cats.into_iter().map(CategoryDto::from).collect()))
67+
}
68+
69+
#[derive(Deserialize)]
70+
pub struct CreateCategoryRequest {
71+
pub name: String,
72+
pub description: Option<String>,
73+
pub emoji: Option<String>,
74+
pub discord_category_id: String,
75+
}
76+
77+
pub async fn create_category_handler(
78+
State(bot_state): State<Arc<Mutex<BotState>>>,
79+
Json(req): Json<CreateCategoryRequest>,
80+
) -> Result<Json<CategoryDto>, (StatusCode, String)> {
81+
if req.name.trim().is_empty() {
82+
return Err((StatusCode::BAD_REQUEST, "Name required".to_string()));
83+
}
84+
if req.discord_category_id.parse::<u64>().is_err() {
85+
return Err((
86+
StatusCode::BAD_REQUEST,
87+
"Invalid discord_category_id".to_string(),
88+
));
89+
}
90+
let p = pool(&bot_state).await?;
91+
92+
let enabled_count = count_enabled_categories(&p).await.map_err(internal)?;
93+
if enabled_count as usize >= CATEGORY_BUTTON_HARD_LIMIT {
94+
return Err((
95+
StatusCode::BAD_REQUEST,
96+
format!("Maximum {} enabled categories", CATEGORY_BUTTON_HARD_LIMIT),
97+
));
98+
}
99+
100+
if let Ok(Some(_)) = get_category_by_name(&req.name, &p).await {
101+
return Err((
102+
StatusCode::CONFLICT,
103+
"Category with this name already exists".to_string(),
104+
));
105+
}
106+
107+
let created = create_category(
108+
&req.name,
109+
req.description.as_deref(),
110+
req.emoji.as_deref(),
111+
&req.discord_category_id,
112+
&p,
113+
)
114+
.await
115+
.map_err(internal)?;
116+
Ok(Json(created.into()))
117+
}
118+
119+
#[derive(Deserialize)]
120+
pub struct UpdateCategoryRequest {
121+
pub name: Option<String>,
122+
pub description: Option<Option<String>>,
123+
pub emoji: Option<Option<String>>,
124+
pub discord_category_id: Option<String>,
125+
pub position: Option<i64>,
126+
pub enabled: Option<bool>,
127+
}
128+
129+
pub async fn update_category_handler(
130+
State(bot_state): State<Arc<Mutex<BotState>>>,
131+
Path(id): Path<String>,
132+
Json(req): Json<UpdateCategoryRequest>,
133+
) -> Result<Json<CategoryDto>, (StatusCode, String)> {
134+
let p = pool(&bot_state).await?;
135+
136+
let existing = get_category_by_id(&id, &p)
137+
.await
138+
.map_err(internal)?
139+
.ok_or((StatusCode::NOT_FOUND, "Category not found".to_string()))?;
140+
141+
if let Some(true) = req.enabled {
142+
if !existing.enabled {
143+
let enabled_count = count_enabled_categories(&p).await.map_err(internal)?;
144+
if enabled_count as usize >= CATEGORY_BUTTON_HARD_LIMIT {
145+
return Err((
146+
StatusCode::BAD_REQUEST,
147+
format!("Maximum {} enabled categories", CATEGORY_BUTTON_HARD_LIMIT),
148+
));
149+
}
150+
}
151+
}
152+
153+
if let Some(ref did) = req.discord_category_id {
154+
if did.parse::<u64>().is_err() {
155+
return Err((
156+
StatusCode::BAD_REQUEST,
157+
"Invalid discord_category_id".to_string(),
158+
));
159+
}
160+
}
161+
162+
update_category(
163+
&id,
164+
req.name.as_deref(),
165+
req.description.as_ref().map(|o| o.as_deref()),
166+
req.emoji.as_ref().map(|o| o.as_deref()),
167+
req.discord_category_id.as_deref(),
168+
req.position,
169+
req.enabled,
170+
&p,
171+
)
172+
.await
173+
.map_err(internal)?;
174+
175+
let updated = get_category_by_id(&id, &p)
176+
.await
177+
.map_err(internal)?
178+
.ok_or((StatusCode::NOT_FOUND, "Category not found".to_string()))?;
179+
Ok(Json(updated.into()))
180+
}
181+
182+
pub async fn delete_category_handler(
183+
State(bot_state): State<Arc<Mutex<BotState>>>,
184+
Path(id): Path<String>,
185+
) -> Result<StatusCode, (StatusCode, String)> {
186+
let p = pool(&bot_state).await?;
187+
let existed = delete_category(&id, &p).await.map_err(internal)?;
188+
if existed {
189+
Ok(StatusCode::NO_CONTENT)
190+
} else {
191+
Err((StatusCode::NOT_FOUND, "Category not found".to_string()))
192+
}
193+
}
194+
195+
#[derive(Serialize, Deserialize)]
196+
pub struct CategorySettingsDto {
197+
pub enabled: bool,
198+
pub selection_timeout_s: i64,
199+
}
200+
201+
impl From<TicketCategorySettings> for CategorySettingsDto {
202+
fn from(s: TicketCategorySettings) -> Self {
203+
Self {
204+
enabled: s.enabled,
205+
selection_timeout_s: s.selection_timeout_s,
206+
}
207+
}
208+
}
209+
210+
pub async fn get_category_settings_handler(
211+
State(bot_state): State<Arc<Mutex<BotState>>>,
212+
) -> Result<Json<CategorySettingsDto>, (StatusCode, String)> {
213+
let p = pool(&bot_state).await?;
214+
let s = get_category_settings(&p).await.map_err(internal)?;
215+
Ok(Json(s.into()))
216+
}
217+
218+
pub async fn update_category_settings_handler(
219+
State(bot_state): State<Arc<Mutex<BotState>>>,
220+
Json(req): Json<CategorySettingsDto>,
221+
) -> Result<Json<CategorySettingsDto>, (StatusCode, String)> {
222+
if req.selection_timeout_s < 30 {
223+
return Err((
224+
StatusCode::BAD_REQUEST,
225+
"selection_timeout_s must be >= 30".to_string(),
226+
));
227+
}
228+
let p = pool(&bot_state).await?;
229+
update_category_settings(req.enabled, req.selection_timeout_s, &p)
230+
.await
231+
.map_err(internal)?;
232+
let s = get_category_settings(&p).await.map_err(internal)?;
233+
Ok(Json(s.into()))
234+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod categories;
2+
3+
pub use categories::*;

crates/rustmail/src/api/handler/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod admin;
22
pub mod apikeys;
33
pub mod auth;
44
pub mod bot;
5+
pub mod categories;
56
pub mod externals;
67
pub mod panel;
78
pub mod user;
@@ -10,6 +11,7 @@ pub use admin::*;
1011
pub use apikeys::*;
1112
pub use auth::*;
1213
pub use bot::*;
14+
pub use categories::*;
1315
pub use externals::*;
1416
pub use panel::*;
1517
pub use user::*;

crates/rustmail/src/api/router.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use tokio::sync::Mutex;
77
pub fn create_api_router(bot_state: Arc<Mutex<BotState>>) -> Router {
88
let admin_router = create_admin_router(bot_state.clone());
99
let apikeys_router = create_apikeys_router(bot_state.clone());
10+
let categories_router = create_categories_router(bot_state.clone());
1011
let bot_router = create_bot_router(bot_state.clone());
1112
let auth_router = create_auth_router();
1213
let panel_router = create_panel_router(bot_state.clone());
@@ -16,6 +17,7 @@ pub fn create_api_router(bot_state: Arc<Mutex<BotState>>) -> Router {
1617
let app = Router::new()
1718
.nest("/api/admin", admin_router)
1819
.nest("/api/apikeys", apikeys_router)
20+
.nest("/api/categories", categories_router)
1921
.nest("/api/bot", bot_router)
2022
.nest("/api/auth", auth_router)
2123
.nest("/api/panel", panel_router)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use crate::prelude::api::*;
2+
use crate::prelude::types::*;
3+
use axum::Router;
4+
use axum::routing::{delete, get, patch, post, put};
5+
use rustmail_types::api::panel_permissions::PanelPermission;
6+
use std::sync::Arc;
7+
use tokio::sync::Mutex;
8+
9+
pub fn create_categories_router(bot_state: Arc<Mutex<BotState>>) -> Router<Arc<Mutex<BotState>>> {
10+
Router::new()
11+
.route("/", get(list_categories_handler))
12+
.route("/", post(create_category_handler))
13+
.route("/{id}", patch(update_category_handler))
14+
.route("/{id}", delete(delete_category_handler))
15+
.route("/settings", get(get_category_settings_handler))
16+
.route("/settings", put(update_category_settings_handler))
17+
.layer(axum::middleware::from_fn_with_state(
18+
bot_state.clone(),
19+
move |state, jar, req, next| {
20+
require_panel_permission(state, jar, req, next, PanelPermission::ManageCategories)
21+
},
22+
))
23+
.layer(axum::middleware::from_fn_with_state(
24+
bot_state,
25+
auth_middleware,
26+
))
27+
}

crates/rustmail/src/api/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod admin;
22
pub mod apikeys;
33
pub mod auth;
44
pub mod bot;
5+
pub mod categories;
56
pub mod externals;
67
pub mod panel;
78
pub mod user;
@@ -10,6 +11,7 @@ pub use admin::*;
1011
pub use apikeys::*;
1112
pub use auth::*;
1213
pub use bot::*;
14+
pub use categories::*;
1315
pub use externals::*;
1416
pub use panel::*;
1517
pub use user::*;

crates/rustmail/src/api/utils/panel_permissions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub async fn get_user_panel_permissions(
7272
PanelPermission::ManageTickets,
7373
PanelPermission::ManageApiKeys,
7474
PanelPermission::ManagePermissions,
75+
PanelPermission::ManageCategories,
7576
];
7677
cache.insert(user_id.to_string(), permissions.clone()).await;
7778
return permissions;
@@ -85,6 +86,7 @@ pub async fn get_user_panel_permissions(
8586
PanelPermission::ManageTickets,
8687
PanelPermission::ManageApiKeys,
8788
PanelPermission::ManagePermissions,
89+
PanelPermission::ManageCategories,
8890
];
8991
}
9092

crates/rustmail_types/src/api/panel_permissions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub enum PanelPermission {
99
ManageTickets,
1010
ManageApiKeys,
1111
ManagePermissions,
12+
ManageCategories,
1213
}
1314

1415
impl PanelPermission {
@@ -20,6 +21,7 @@ impl PanelPermission {
2021
PanelPermission::ManageTickets => "manage_tickets",
2122
PanelPermission::ManageApiKeys => "manage_api_keys",
2223
PanelPermission::ManagePermissions => "manage_permissions",
24+
PanelPermission::ManageCategories => "manage_categories",
2325
}
2426
}
2527

@@ -31,6 +33,7 @@ impl PanelPermission {
3133
"manage_tickets" => Some(PanelPermission::ManageTickets),
3234
"manage_api_keys" => Some(PanelPermission::ManageApiKeys),
3335
"manage_permissions" => Some(PanelPermission::ManagePermissions),
36+
"manage_categories" => Some(PanelPermission::ManageCategories),
3437
_ => None,
3538
}
3639
}

0 commit comments

Comments
 (0)