feat: add activity logs audit endpoint, payment notifications, gateway routing
- Add activity_logs handler with paginated admin API - Register /api/admin/activity-logs route in users service - Add gateway routing for activity-logs to users service - Trigger notification on successful tracecoin purchase - Update handlers mod to include activity_logs module
This commit is contained in:
parent
ab25f7a994
commit
d996131890
5 changed files with 156 additions and 2 deletions
|
|
@ -97,6 +97,7 @@ impl Services {
|
|||
|| path.starts_with("/api/admin/discounts")
|
||||
|| path.starts_with("/api/admin/tracecoin-packages")
|
||||
|| path.starts_with("/api/admin/reports")
|
||||
|| path.starts_with("/api/admin/activity-logs")
|
||||
{
|
||||
Some(self.users_url.clone())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,8 +231,24 @@ async fn verify_payment(
|
|||
.await
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Send notification to user about successful purchase
|
||||
let _ = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
payment.user_id,
|
||||
"Tracecoins Purchased Successfully",
|
||||
format!("Your {} Tracecoin package has been credited to your wallet.", payment.tracecoins_credited),
|
||||
"PAYMENT",
|
||||
payment.id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(Json(VerifyPaymentResponse {
|
||||
verified: true,
|
||||
|
|
|
|||
134
apps/users/src/handlers/activity_logs.rs
Normal file
134
apps/users/src/handlers/activity_logs.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new().route("/", get(list_activity_logs))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
actor_id: Option<Uuid>,
|
||||
entity_type: Option<String>,
|
||||
action: Option<String>,
|
||||
from: Option<DateTime<Utc>>,
|
||||
to: Option<DateTime<Utc>>,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::FromRow)]
|
||||
struct ActivityLogDto {
|
||||
id: Uuid,
|
||||
actor_id: Uuid,
|
||||
actor_type: String,
|
||||
entity_id: Uuid,
|
||||
entity_type: String,
|
||||
action: String,
|
||||
metadata: Option<serde_json::Value>,
|
||||
ip_address: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PaginatedResponse {
|
||||
data: Vec<ActivityLogDto>,
|
||||
total: i64,
|
||||
page: i64,
|
||||
total_pages: i64,
|
||||
}
|
||||
|
||||
async fn list_activity_logs(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ListQuery>,
|
||||
) -> impl IntoResponse {
|
||||
// Ensure admin permission (require_admin will be applied by router if nested under /api/admin)
|
||||
let page = params.page.unwrap_or(1).max(1);
|
||||
let limit = params.limit.unwrap_or(50).clamp(1, 100);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let actor_id = params.actor_id;
|
||||
let entity_type = params.entity_type.as_deref().unwrap_or_default();
|
||||
let action = params.action.as_deref().unwrap_or_default();
|
||||
let from_dt = params.from;
|
||||
let to_dt = params.to;
|
||||
|
||||
let total: i64 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM activity_logs
|
||||
WHERE ($1::uuid IS NULL OR actor_id = $1)
|
||||
AND ($2::text IS NULL OR entity_type = $2)
|
||||
AND ($3::text IS NULL OR action = $3)
|
||||
AND ($4::timestamptz IS NULL OR created_at >= $4)
|
||||
AND ($5::timestamptz IS NULL OR created_at <= $5)
|
||||
"#,
|
||||
actor_id,
|
||||
if entity_type.is_empty() { None } else { Some(entity_type) },
|
||||
if action.is_empty() { None } else { Some(action) },
|
||||
from_dt,
|
||||
to_dt,
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
let rows = sqlx::query_as::<_, ActivityLogDto>(
|
||||
r#"
|
||||
SELECT id, actor_id, actor_type, entity_id, entity_type, action, metadata, ip_address, user_agent, created_at
|
||||
FROM activity_logs
|
||||
WHERE ($1::uuid IS NULL OR actor_id = $1)
|
||||
AND ($2::text IS NULL OR entity_type = $2)
|
||||
AND ($3::text IS NULL OR action = $3)
|
||||
AND ($4::timestamptz IS NULL OR created_at >= $4)
|
||||
AND ($5::timestamptz IS NULL OR created_at <= $5)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $6 OFFSET $7
|
||||
"#
|
||||
)
|
||||
.bind(actor_id)
|
||||
.bind(if entity_type.is_empty() { None } else { Some(entity_type) })
|
||||
.bind(if action.is_empty() { None } else { Some(action) })
|
||||
.bind(from_dt)
|
||||
.bind(to_dt)
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
match rows {
|
||||
Ok(data) => {
|
||||
let total_pages = if total == 0 { 1 } else { (total + limit - 1) / limit };
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(PaginatedResponse {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
total_pages,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch activity logs: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to fetch activity logs" })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod admin;
|
||||
pub mod activity_logs;
|
||||
pub mod approvals;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ async fn main() {
|
|||
.nest("/api/admin/dashboard-config", handlers::config::dashboard_router())
|
||||
.nest("/api/admin/dashboard", handlers::dashboard::router())
|
||||
.nest("/api/admin/runtime-configs", handlers::config::admin_runtime_router())
|
||||
// ── Admin: Activity Logs (Audit) ───────────────────────────────────
|
||||
.nest("/api/admin/activity-logs", handlers::activity_logs::router())
|
||||
// ── Public Config ─────────────────────────────────────────────────
|
||||
.nest("/api/config/onboarding", handlers::config::onboarding_router())
|
||||
.nest("/api/config/dashboard", handlers::config::dashboard_router())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue