//! HTTP-роуты ролевой админки. Named-guard'ы (`owner_guard`/`staff_guard`/ //! `partner_guard`) уже проверяют роль до входа в хендлер — сами хендлеры //! читают текущего юзера только через `Extension`. use axum::{ extract::{Extension, Path, State}, http::StatusCode, middleware, routing::{get, patch, post}, Json, Router, }; use serde::Deserialize; use serde_json::{json, Value}; use uuid::Uuid; use crate::{ api::ApiState, db::models::User, error::AppError, modules::auth::{auth_guard, owner_guard, partner_guard, staff_guard}, }; /// Любой залогиненный видит свою роль/права — нужно фронту, чтобы решить, /// что показать и не пора ли редиректить на `/admin/login`. pub fn router(state: ApiState) -> Router { Router::new() .route("/me", get(get_my_role)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } /// Только Owner/Moderator — гейт для Caddy `forward_auth` перед Grafana /// (см. netrunner-data/observability/Caddyfile). Тело ответа не важно, важен /// только статус: 200 — Caddy пускает дальше в Grafana, 401 — блокирует. /// Бизнес-метрики (выручка, число юзеров) чувствительны — Support/Partner /// сюда не допускаются, поэтому `staff_guard`, а не `support_access_guard`. pub fn observability_router(state: ApiState) -> Router { Router::new() .route("/check", get(|| async { StatusCode::OK })) .route_layer(middleware::from_fn_with_state(state.clone(), staff_guard)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } /// Только Owner: создание модераторов/саппортов, выдача модераторам права /// на управление нодами. pub fn owner_router(state: ApiState) -> Router { Router::new() .route("/moderators", post(create_moderator)) .route("/support", post(create_support)) .route("/users/{id}/permissions", patch(set_permissions)) .route_layer(middleware::from_fn_with_state(state.clone(), owner_guard)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } /// Owner + Moderator: создание партнёров, список staff-юзеров, заявки на вывод. pub fn staff_router(state: ApiState) -> Router { Router::new() .route("/partners", post(create_partner)) .route("/staff-users", get(list_staff)) .route("/withdrawals", get(list_withdrawals)) .route("/withdrawals/{id}", patch(update_withdrawal)) .route_layer(middleware::from_fn_with_state(state.clone(), staff_guard)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } /// Только Partner: свой заработок и заявки на вывод. pub fn partner_router(state: ApiState) -> Router { Router::new() .route("/withdrawals", post(request_withdrawal)) .route("/earnings", get(get_my_earnings)) .route_layer(middleware::from_fn_with_state(state.clone(), partner_guard)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } async fn get_my_role(Extension(user): Extension) -> Json { Json(json!({ "role": user.role, "can_manage_nodes": user.can_manage_nodes, "username": user.username, "partner_code": user.partner_code, "commission_percent": user.commission_percent, })) } #[derive(Deserialize)] pub struct CreateModeratorPayload { pub username: String, pub password: String, pub can_manage_nodes: bool, } async fn create_moderator( State(state): State, Json(p): Json, ) -> Result, AppError> { let user = state .staff_service .create_moderator(&p.username, &p.password, p.can_manage_nodes) .await?; Ok(Json(json!({ "id": user.id, "username": user.username, "role": user.role, "can_manage_nodes": user.can_manage_nodes, }))) } #[derive(Deserialize)] pub struct CreateSupportPayload { pub username: String, pub password: String, } async fn create_support( State(state): State, Json(p): Json, ) -> Result, AppError> { let user = state .staff_service .create_support(&p.username, &p.password) .await?; Ok(Json(json!({ "id": user.id, "username": user.username, "role": user.role, }))) } #[derive(Deserialize)] pub struct CreatePartnerPayload { pub username: String, pub password: String, pub commission_percent: rust_decimal::Decimal, } async fn create_partner( State(state): State, Json(p): Json, ) -> Result, AppError> { let user = state .staff_service .create_partner(&p.username, &p.password, p.commission_percent) .await?; Ok(Json(json!({ "id": user.id, "username": user.username, "partner_code": user.partner_code, "commission_percent": user.commission_percent, }))) } #[derive(Deserialize)] pub struct SetPermissionsPayload { pub can_manage_nodes: bool, } async fn set_permissions( State(state): State, Path(id): Path, Json(p): Json, ) -> Result, AppError> { let user = state .staff_service .set_can_manage_nodes(id, p.can_manage_nodes) .await? .ok_or(AppError::UserNotFound)?; Ok(Json(json!({ "id": user.id, "can_manage_nodes": user.can_manage_nodes, }))) } async fn list_staff(State(state): State) -> Result>, AppError> { let users = state.staff_service.list_staff().await?; Ok(Json( users .into_iter() .map(|u| { json!({ "id": u.id, "username": u.username, "role": u.role, "can_manage_nodes": u.can_manage_nodes, "commission_percent": u.commission_percent, "partner_code": u.partner_code, }) }) .collect(), )) } async fn list_withdrawals(State(state): State) -> Result, AppError> { let list = state.staff_service.list_all_withdrawals().await?; Ok(Json(json!(list))) } #[derive(Deserialize)] pub struct UpdateWithdrawalPayload { pub status: String, } async fn update_withdrawal( Extension(user): Extension, State(state): State, Path(id): Path, Json(p): Json, ) -> Result, AppError> { if !["approved", "rejected", "paid"].contains(&p.status.as_str()) { return Err(AppError::BusinessLogic("Недопустимый статус.".into())); } let req = state .staff_service .update_withdrawal(id, &p.status, user.id) .await? .ok_or_else(|| AppError::NotFound("Заявка не найдена".into()))?; Ok(Json(json!(req))) } async fn get_my_earnings( Extension(user): Extension, State(state): State, ) -> Result, AppError> { let earnings = state.staff_service.get_earnings(user.id).await?; let withdrawals = state .staff_service .list_withdrawals_for_partner(user.id) .await?; Ok(Json(json!({ "earnings": earnings, "withdrawals": withdrawals, "partner_code": user.partner_code, }))) } #[derive(Deserialize)] pub struct RequestWithdrawalPayload { pub currency: String, pub amount: i64, pub note: Option, } async fn request_withdrawal( Extension(user): Extension, State(state): State, Json(p): Json, ) -> Result, AppError> { let req = state .staff_service .request_withdrawal(user.id, &p.currency, p.amount, p.note.as_deref()) .await?; Ok(Json(json!(req))) }