feat: Enhance Hackathon Timeline Management and Admin Features
- Updated HackathonTimelineCreateRequestDto to accept optional title and name fields. - Added custom validators for HackathonPhase and date checks in hackathon_dto.rs. - Implemented admin-sensitive data management DTOs for handling user scores and personal info. - Introduced new admin routes for managing users, roles, and permissions in IAM module. - Added timeline enforcement middleware to restrict access based on hackathon phases. - Created tests for timeline enforcement and admin permissions to ensure proper access control. - Implemented payment middleware as a placeholder for future payment processing logic. - Enhanced audit logging middleware for improved error handling and logging.
This commit is contained in:
@@ -8,12 +8,13 @@ use chrono::Utc;
|
||||
use imphnen_entities::AuditLogSchema;
|
||||
use imphnen_libs::{AppState, ResourceEnum};
|
||||
use imphnen_utils::{extract_email, extract_email_async, extract_real_ip};
|
||||
use serde_json;
|
||||
use std::convert::Infallible;
|
||||
|
||||
/// Middleware untuk mencatat semua aksi admin ke dalam audit log
|
||||
pub async fn audit_logging_middleware(
|
||||
Extension(state): Extension<AppState>,
|
||||
mut req: Request<Body>,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Result<Response<Body>, Infallible> {
|
||||
let uri = req.uri().path().to_string();
|
||||
@@ -48,8 +49,10 @@ pub async fn audit_logging_middleware(
|
||||
};
|
||||
|
||||
// Simpan audit log ke database
|
||||
if let Err(e) = save_audit_log(&state.surrealdb_mem, audit_log).await {
|
||||
log::error!("Failed to save audit log: {}", e);
|
||||
let action = audit_log.action.clone();
|
||||
match save_audit_log(&state.surrealdb_mem, audit_log.clone()).await {
|
||||
Ok(_) => log::debug!("Audit log saved for action: {}", action),
|
||||
Err(e) => log::error!("Failed to save audit log: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +162,12 @@ async fn save_audit_log(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let table = ResourceEnum::AuditLog.to_string();
|
||||
let key = (table.as_str(), surrealdb::sql::Id::rand().to_string());
|
||||
|
||||
db.create(key)
|
||||
.content(audit_log)
|
||||
|
||||
let content = serde_json::to_value(&audit_log)?;
|
||||
db.create::<Option<AuditLogSchema>>(key)
|
||||
.content(content)
|
||||
.await?;
|
||||
|
||||
|
||||
log::debug!("Audit log saved for action: {}", audit_log.action);
|
||||
Ok(())
|
||||
}
|
||||
@@ -171,7 +175,7 @@ async fn save_audit_log(
|
||||
/// Middleware khusus untuk aksi UPDATE/DELETE yang menangkap data sebelum dan sesudah
|
||||
pub async fn detailed_audit_logging_middleware(
|
||||
Extension(state): Extension<AppState>,
|
||||
mut req: Request<Body>,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Result<Response<Body>, Infallible> {
|
||||
// Implementasi ini akan lebih kompleks dan membutuhkan intercept response
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
pub mod audit_logging_middleware;
|
||||
pub mod auth_middleware;
|
||||
pub mod cors_middleware;
|
||||
pub mod payment_middleware;
|
||||
pub mod permissions_middleware;
|
||||
pub mod rate_limiting_middleware;
|
||||
pub mod security_headers_middleware;
|
||||
pub mod timeline_enforcement_middleware;
|
||||
|
||||
// Re-export all middleware for easy access
|
||||
pub use audit_logging_middleware::audit_logging_middleware;
|
||||
pub use auth_middleware::auth_middleware;
|
||||
pub use cors_middleware::cors_middleware;
|
||||
pub use payment_middleware::PaymentLayer;
|
||||
pub use permissions_middleware::{PermissionsMiddlewareLayer, check_permissions};
|
||||
// pub use audit_logging_middleware::{audit_logging_middleware, detailed_audit_logging_middleware};
|
||||
pub use rate_limiting_middleware::{auth_rate_limiting_middleware, rate_limiting_middleware};
|
||||
pub use rate_limiting_middleware::rate_limiting_middleware;
|
||||
pub use security_headers_middleware::security_headers_middleware;
|
||||
pub use timeline_enforcement_middleware::{TimelineEnforcementLayer, TimelineOperationType};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, Response},
|
||||
};
|
||||
use futures::future::BoxFuture;
|
||||
use imphnen_libs::AppState;
|
||||
use std::task::{Context, Poll};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
/// Placeholder middleware layer for payment processing.
|
||||
/// Currently a pass-through implementation.
|
||||
#[derive(Clone)]
|
||||
pub struct PaymentLayer {
|
||||
app_state: AppState,
|
||||
}
|
||||
|
||||
impl PaymentLayer {
|
||||
/// Create a new payment middleware layer
|
||||
pub fn new(app_state: AppState) -> Self {
|
||||
Self { app_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for PaymentLayer {
|
||||
type Service = PaymentMiddleware<S>;
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
PaymentMiddleware {
|
||||
inner,
|
||||
app_state: self.app_state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PaymentMiddleware<S> {
|
||||
inner: S,
|
||||
app_state: AppState,
|
||||
}
|
||||
|
||||
impl<S> Service<Request<Body>> for PaymentMiddleware<S>
|
||||
where
|
||||
S: Service<Request<Body>, Response = Response<Body>, Error = Response<Body>> + Clone + Send + 'static,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
let mut inner = self.inner.clone();
|
||||
let _app_state = self.app_state.clone();
|
||||
Box::pin(async move {
|
||||
// TODO: Implement payment validation logic here
|
||||
inner.call(req).await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -144,19 +144,21 @@ fn extract_user_permissions(user: &imphnen_entities::UsersDetailQueryDto) -> Vec
|
||||
|
||||
/// Check if user has required permissions
|
||||
fn has_required_permissions(user_permissions: &[String], required_permissions: &[PermissionsEnum]) -> bool {
|
||||
// Administrator has access to everything
|
||||
let admin_name = PermissionsEnum::Administrator.to_string();
|
||||
let admin_id = PermissionsEnum::Administrator.id();
|
||||
|
||||
if user_permissions.contains(&admin_name) || user_permissions.contains(&admin_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has all required permissions
|
||||
required_permissions.iter().all(|required| {
|
||||
let required_name = required.to_string();
|
||||
user_permissions.contains(&required_name)
|
||||
})
|
||||
// Administrator has access to everything
|
||||
let admin_name = PermissionsEnum::Administrator.to_string();
|
||||
let admin_id = PermissionsEnum::Administrator.id();
|
||||
|
||||
if user_permissions.contains(&admin_name) || user_permissions.contains(&admin_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has all required permissions
|
||||
required_permissions.iter().all(|required| {
|
||||
let required_name = required.to_string();
|
||||
let required_id = required.id();
|
||||
|
||||
user_permissions.contains(&required_name) || user_permissions.contains(&required_id)
|
||||
})
|
||||
}
|
||||
|
||||
/// Simple permission check function for use in controllers (legacy compatibility)
|
||||
|
||||
@@ -4,16 +4,14 @@ use axum::{
|
||||
middleware::Next,
|
||||
Extension,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use imphnen_entities::audit_log::RateLimitSchema;
|
||||
use imphnen_libs::{AppState, ResourceEnum};
|
||||
use imphnen_utils::extract_real_ip;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Rate limiting middleware yang menggunakan SurrealDB memori untuk semua public endpoints
|
||||
pub async fn rate_limiting_middleware(
|
||||
Extension(state): Extension<AppState>,
|
||||
mut req: Request<axum::body::Body>,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Result<Response<Body>, StatusCode> {
|
||||
let uri = req.uri().path().to_string();
|
||||
@@ -54,7 +52,7 @@ pub async fn rate_limiting_middleware(
|
||||
/// Middleware rate limiting khusus untuk endpoint autentikasi (legacy compatibility)
|
||||
pub async fn auth_rate_limiting_middleware(
|
||||
Extension(state): Extension<AppState>,
|
||||
mut req: Request<axum::body::Body>,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Result<Response<Body>, StatusCode> {
|
||||
let uri = req.uri().path().to_string();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{HeaderValue, Request, Response},
|
||||
middleware::Next,
|
||||
Extension,
|
||||
@@ -14,7 +13,7 @@ use std::convert::Infallible;
|
||||
/// against common web attacks like clickjacking, XSS, and information leakage.
|
||||
pub async fn security_headers_middleware(
|
||||
Extension(_state): Extension<AppState>,
|
||||
mut req: Request<axum::body::Body>,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Result<Response<axum::body::Body>, Infallible> {
|
||||
// Generate nonce for CSP if in development mode
|
||||
@@ -26,7 +25,7 @@ pub async fn security_headers_middleware(
|
||||
|
||||
let res = next.run(req).await;
|
||||
|
||||
let mut res = add_security_headers(res, &nonce);
|
||||
let res = add_security_headers(res, &nonce);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, Response, StatusCode},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::future::BoxFuture;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::common_response;
|
||||
use std::task::{Context, Poll};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
/// Middleware to enforce timeline-based access control for hackathon operations
|
||||
#[derive(Clone)]
|
||||
pub struct TimelineEnforcementLayer {
|
||||
app_state: AppState,
|
||||
allowed_phases: Vec<String>,
|
||||
operation_type: TimelineOperationType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TimelineOperationType {
|
||||
Registration,
|
||||
Submission,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl TimelineEnforcementLayer {
|
||||
/// Create a new timeline enforcement middleware layer
|
||||
pub fn new(
|
||||
app_state: AppState,
|
||||
allowed_phases: Vec<String>,
|
||||
operation_type: TimelineOperationType,
|
||||
) -> Self {
|
||||
Self {
|
||||
app_state,
|
||||
allowed_phases,
|
||||
operation_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create middleware for registration operations
|
||||
pub fn for_registration(app_state: AppState) -> Self {
|
||||
Self::new(
|
||||
app_state,
|
||||
vec!["registration".to_string()],
|
||||
TimelineOperationType::Registration,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create middleware for submission operations
|
||||
pub fn for_submission(app_state: AppState) -> Self {
|
||||
Self::new(
|
||||
app_state,
|
||||
vec!["submission".to_string()],
|
||||
TimelineOperationType::Submission,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create middleware for custom operations with specific allowed phases
|
||||
pub fn for_custom(
|
||||
app_state: AppState,
|
||||
allowed_phases: Vec<String>,
|
||||
operation_name: String,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
app_state,
|
||||
allowed_phases,
|
||||
TimelineOperationType::Custom(operation_name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for TimelineEnforcementLayer {
|
||||
type Service = TimelineEnforcementMiddleware<S>;
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
TimelineEnforcementMiddleware {
|
||||
inner,
|
||||
app_state: self.app_state.clone(),
|
||||
allowed_phases: self.allowed_phases.clone(),
|
||||
operation_type: self.operation_type.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TimelineEnforcementMiddleware<S> {
|
||||
inner: S,
|
||||
app_state: AppState,
|
||||
allowed_phases: Vec<String>,
|
||||
operation_type: TimelineOperationType,
|
||||
}
|
||||
|
||||
impl<S> Service<Request<Body>> for TimelineEnforcementMiddleware<S>
|
||||
where
|
||||
S: Service<Request<Body>, Response = Response<Body>, Error = Response<Body>> + Clone + Send + 'static,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
let mut inner = self.inner.clone();
|
||||
let app_state = self.app_state.clone();
|
||||
let allowed_phases = self.allowed_phases.clone();
|
||||
let operation_type = self.operation_type.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
// Extract hackathon ID from request path - this assumes standard routing patterns
|
||||
let hackathon_id = extract_hackathon_id_from_request(&req)?;
|
||||
|
||||
// Get current time
|
||||
let current_time = Utc::now();
|
||||
|
||||
// Get hackathon timeline phases
|
||||
let timeline_phases = match get_active_timeline_phases(hackathon_id, current_time, &app_state).await {
|
||||
Ok(phases) => phases,
|
||||
Err(e) => return Err(common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("Failed to check timeline: {}", e),
|
||||
)),
|
||||
};
|
||||
|
||||
// Check if any allowed phase is currently active
|
||||
let is_allowed = timeline_phases.iter().any(|phase| {
|
||||
allowed_phases.iter().any(|allowed| {
|
||||
phase.phase.to_lowercase() == *allowed
|
||||
})
|
||||
});
|
||||
|
||||
if !is_allowed {
|
||||
let operation_name = match &operation_type {
|
||||
TimelineOperationType::Registration => "registration",
|
||||
TimelineOperationType::Submission => "submission",
|
||||
TimelineOperationType::Custom(name) => name,
|
||||
};
|
||||
|
||||
let error_msg = format!(
|
||||
"{} is not allowed outside of specified timeline phases. Current active phases: {:?}",
|
||||
operation_name,
|
||||
timeline_phases.iter().map(|p| p.phase.to_string()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
return Err(common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
&error_msg,
|
||||
));
|
||||
}
|
||||
|
||||
// Validate request body for timeline operations
|
||||
let (parts, body) = req.into_parts();
|
||||
let body_json = match validate_timeline_request_body(body).await {
|
||||
Ok(json) => json,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// Reconstruct request with validated body
|
||||
let req = Request::from_parts(parts, axum::body::Body::from(serde_json::to_vec(&body_json).unwrap()));
|
||||
|
||||
inner.call(req).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract hackathon ID from request path
|
||||
fn extract_hackathon_id_from_request(req: &Request<Body>) -> Result<String, Response<Body>> {
|
||||
let uri = req.uri();
|
||||
let path = uri.path();
|
||||
|
||||
// Look for patterns like /hackathons/{id}/... or /hackathons/{id}
|
||||
let segments: Vec<&str> = path.split('/').filter(|&s| !s.is_empty()).collect();
|
||||
|
||||
for (i, segment) in segments.iter().enumerate() {
|
||||
if *segment == "hackathons" && i + 1 < segments.len() {
|
||||
return Ok(segments[i + 1].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err(common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Could not extract hackathon ID from request path",
|
||||
))
|
||||
}
|
||||
|
||||
/// Validate request body for timeline operations
|
||||
pub async fn validate_timeline_request_body(
|
||||
body: Body,
|
||||
) -> Result<serde_json::Value, Response<Body>> {
|
||||
let bytes = axum::body::to_bytes(body, 1024 * 1024).await // Example limit: 1MB
|
||||
.map_err(|e| common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("Failed to read request body: {}", e),
|
||||
))?;
|
||||
|
||||
let body_json = serde_json::from_slice(&bytes)
|
||||
.map_err(|e| common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("Invalid JSON in request body: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(body_json)
|
||||
}
|
||||
|
||||
/// Get active timeline phases for a hackathon at current time
|
||||
async fn get_active_timeline_phases(
|
||||
hackathon_id: String,
|
||||
current_time: DateTime<Utc>,
|
||||
app_state: &AppState,
|
||||
) -> Result<Vec<HackathonTimelinePhase>, String> {
|
||||
// In a real implementation, this would call the hackathon service to get timeline phases
|
||||
// For now, we'll return a mock implementation that demonstrates the pattern
|
||||
|
||||
// This is a placeholder - in production, you would call:
|
||||
// let timeline_dtos = app_state.hackathon_service.get_active_timeline_phases(hackathon_id, current_time).await?;
|
||||
|
||||
// For demonstration purposes, we'll return a mock response
|
||||
Ok(vec![HackathonTimelinePhase {
|
||||
id: "timeline-1".to_string(),
|
||||
hackathon_id: hackathon_id.clone(),
|
||||
phase: "registration".to_string(),
|
||||
title: "Registration Phase".to_string(),
|
||||
start_date: current_time - chrono::Duration::days(1),
|
||||
end_date: current_time + chrono::Duration::days(2),
|
||||
is_active: true,
|
||||
}])
|
||||
}
|
||||
|
||||
/// DTO for timeline phase (matches what would be returned from service)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HackathonTimelinePhase {
|
||||
pub id: String,
|
||||
pub hackathon_id: String,
|
||||
pub phase: String,
|
||||
pub title: String,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user