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
1 change: 1 addition & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 3 additions & 6 deletions backend/migrations/20260221120000_add_plans_and_logs.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
-- Migration: Add plan_logs table
-- NOTE:
-- The plans table is already created in the initial migration.
-- This migration should only add the dependent plan_logs table.
-- Migration: Add plan_logs table (plans already created in init)

CREATE TABLE plan_logs (
CREATE TABLE IF NOT EXISTS plan_logs (
id SERIAL PRIMARY KEY,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
action VARCHAR(64) NOT NULL,
Expand All @@ -12,4 +9,4 @@ CREATE TABLE plan_logs (
);

CREATE INDEX idx_plan_logs_plan_id ON plan_logs(plan_id);
CREATE INDEX idx_plan_logs_performed_by ON plan_logs(performed_by);
CREATE INDEX idx_plan_logs_performed_by ON plan_logs(performed_by);
13 changes: 13 additions & 0 deletions backend/migrations/20260324160000_extend_action_logs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Extend action_logs table for detailed audit logging
-- Add admin_id for admin-specific actions
ALTER TABLE action_logs ADD COLUMN IF NOT EXISTS admin_id UUID REFERENCES admins(id) ON DELETE SET NULL;

-- Add value tracking for parameter updates
ALTER TABLE action_logs ADD COLUMN IF NOT EXISTS old_value TEXT;
ALTER TABLE action_logs ADD COLUMN IF NOT EXISTS new_value TEXT;

-- Add metadata for additional context
ALTER TABLE action_logs ADD COLUMN IF NOT EXISTS metadata JSONB;

-- Create index for admin_id
CREATE INDEX IF NOT EXISTS idx_action_logs_admin_id ON action_logs(admin_id);
84 changes: 36 additions & 48 deletions backend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ use crate::governance::{
use crate::insurance_fund::{CreateInsuranceClaimRequest, ProcessInsuranceClaimRequest};
use crate::legacy_content::{ContentListFilters, LegacyContentService};
use crate::loan_lifecycle::{CreateLoanRequest, LoanLifecycleService, LoanListFilters};
use crate::message_access_audit::{
MessageAccessAuditService, MessageAuditFilters,
};
use crate::message_access_audit::{MessageAccessAuditService, MessageAuditFilters};
use crate::secure_messages::{
CreateLegacyMessageRequest, LegacyMessageDeliveryService, MessageEncryptionService,
MessageKeyService,
Expand Down Expand Up @@ -146,10 +144,7 @@ pub async fn create_app(db: PgPool, config: Config) -> Result<Router, ApiError>
"/api/admin/messages/delivery/process",
post(process_legacy_message_delivery),
)
.route(
"/api/admin/messages/audit",
get(get_message_audit_logs),
)
.route("/api/admin/messages/audit", get(get_message_audit_logs))
.route(
"/api/admin/messages/audit/summary",
get(get_message_audit_summary),
Expand Down Expand Up @@ -470,26 +465,14 @@ pub async fn create_app(db: PgPool, config: Config) -> Result<Router, ApiError>
)
.route("/api/will/audit/my-activity", get(get_my_audit_activity))
// -- Legacy Content Upload (Issue #XXX) -------------------------------
.route(
"/api/content/upload",
post(upload_legacy_content),
)
.route(
"/api/content",
get(list_user_content),
)
.route("/api/content/upload", post(upload_legacy_content))
.route("/api/content", get(list_user_content))
.route(
"/api/content/:content_id",
get(get_content_by_id).delete(delete_content),
)
.route(
"/api/content/:content_id/download",
get(download_content),
)
.route(
"/api/content/stats",
get(get_storage_stats),
)
.route("/api/content/:content_id/download", get(download_content))
.route("/api/content/stats", get(get_storage_stats))
.with_state(state);

// Add price feed routes with separate state
Expand Down Expand Up @@ -571,7 +554,7 @@ async fn get_plan(
"status": "success",
"data": p
}))),
None => Err(ApiError::NotFound(format!("Plan {} not found", plan_id))),
None => Err(ApiError::NotFound(format!("Plan {plan_id} not found"))),
}
}

Expand Down Expand Up @@ -602,8 +585,7 @@ async fn get_due_for_claim_plan(
"data": plan
}))),
None => Err(ApiError::NotFound(format!(
"Plan {} not found or not due for claim",
plan_id
"Plan {plan_id} not found or not due for claim"
))),
}
}
Expand Down Expand Up @@ -679,7 +661,9 @@ async fn get_message_audit_logs(
Query(filters): Query<MessageAuditFilters>,
) -> Result<Json<Value>, ApiError> {
let logs = MessageAccessAuditService::get_logs(&state.db, &filters).await?;
Ok(Json(json!({ "status": "success", "data": logs, "count": logs.len() })))
Ok(Json(
json!({ "status": "success", "data": logs, "count": logs.len() }),
))
}

async fn get_message_audit_summary(
Expand All @@ -696,9 +680,10 @@ async fn search_message_audit_logs(
Query(params): Query<SearchAuditParams>,
) -> Result<Json<Value>, ApiError> {
let limit = params.limit.unwrap_or(100);
let logs =
MessageAccessAuditService::search_logs(&state.db, &params.q, limit).await?;
Ok(Json(json!({ "status": "success", "data": logs, "count": logs.len() })))
let logs = MessageAccessAuditService::search_logs(&state.db, &params.q, limit).await?;
Ok(Json(
json!({ "status": "success", "data": logs, "count": logs.len() }),
))
}

#[derive(Debug, serde::Deserialize)]
Expand All @@ -712,17 +697,17 @@ async fn get_message_access_history(
AuthenticatedUser(_user): AuthenticatedUser,
Path(message_id): Path<Uuid>,
) -> Result<Json<Value>, ApiError> {
let logs =
MessageAccessAuditService::get_message_logs(&state.db, message_id, None).await?;
Ok(Json(json!({ "status": "success", "data": logs, "count": logs.len() })))
let logs = MessageAccessAuditService::get_message_logs(&state.db, message_id, None).await?;
Ok(Json(
json!({ "status": "success", "data": logs, "count": logs.len() }),
))
}

async fn get_my_message_activity(
State(state): State<Arc<AppState>>,
AuthenticatedUser(user): AuthenticatedUser,
) -> Result<Json<Value>, ApiError> {
let activity =
MessageAccessAuditService::get_user_activity(&state.db, user.user_id).await?;
let activity = MessageAccessAuditService::get_user_activity(&state.db, user.user_id).await?;
Ok(Json(json!({ "status": "success", "data": activity })))
}

Expand Down Expand Up @@ -974,8 +959,7 @@ async fn get_simulation(
"data": sim
}))),
None => Err(ApiError::NotFound(format!(
"Simulation {} not found",
simulation_id
"Simulation {simulation_id} not found"
))),
}
}
Expand Down Expand Up @@ -2238,18 +2222,19 @@ async fn upload_legacy_content(
) -> Result<Json<Value>, ApiError> {
// Validate the content type
LegacyContentService::validate_content_type(&req.content_type)?;

// For now, we'll create a metadata record. Full implementation would handle file upload.
let metadata = crate::legacy_content::UploadMetadata {
original_filename: req.original_filename,
content_type: req.content_type.clone(),
file_size: 0, // Would be set from actual file upload
description: req.description,
};

let storage_path = LegacyContentService::generate_storage_path(user.user_id, &metadata.original_filename);

let storage_path =
LegacyContentService::generate_storage_path(user.user_id, &metadata.original_filename);
let file_hash = "pending".to_string(); // Would be calculated from file content

let content = LegacyContentService::create_content_record(
&state.db,
user.user_id,
Expand All @@ -2258,7 +2243,7 @@ async fn upload_legacy_content(
file_hash,
)
.await?;

Ok(Json(json!({
"status": "success",
"data": content
Expand All @@ -2273,7 +2258,8 @@ async fn list_user_content(
AuthenticatedUser(user): AuthenticatedUser,
Query(filters): Query<ContentListFilters>,
) -> Result<Json<Value>, ApiError> {
let contents = LegacyContentService::list_user_content(&state.db, user.user_id, &filters).await?;
let contents =
LegacyContentService::list_user_content(&state.db, user.user_id, &filters).await?;
Ok(Json(json!({
"status": "success",
"data": contents,
Expand All @@ -2289,7 +2275,8 @@ async fn get_content_by_id(
Path(content_id): Path<Uuid>,
AuthenticatedUser(user): AuthenticatedUser,
) -> Result<Json<Value>, ApiError> {
let content = LegacyContentService::get_content_by_id(&state.db, content_id, user.user_id).await?;
let content =
LegacyContentService::get_content_by_id(&state.db, content_id, user.user_id).await?;
Ok(Json(json!({
"status": "success",
"data": content
Expand Down Expand Up @@ -2319,15 +2306,16 @@ async fn download_content(
Path(content_id): Path<Uuid>,
AuthenticatedUser(user): AuthenticatedUser,
) -> Result<axum::response::Response, ApiError> {
let content = LegacyContentService::get_content_by_id(&state.db, content_id, user.user_id).await?;

let content =
LegacyContentService::get_content_by_id(&state.db, content_id, user.user_id).await?;

// In a full implementation, this would read from the FileStorageService
// For now, return a placeholder response
use axum::body::Body;
use axum::http::{header, Response, StatusCode};

let content_disposition = format!("attachment; filename=\"{}\"", content.original_filename);

Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, &content.content_type)
Expand Down
14 changes: 9 additions & 5 deletions backend/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::api_error::ApiError;
use crate::app::AppState;
use crate::config::Config;
use crate::notifications::AuditLogService;
use crate::notifications::{audit_action, entity_type, AuditLogService};
use axum::{extract::State, Json};
use bcrypt::verify;
use chrono::{DateTime, Duration, Utc};
Expand Down Expand Up @@ -353,7 +353,7 @@ pub async fn send_2fa(

// 3. Hash OTP
let otp_hash = bcrypt::hash(&otp, bcrypt::DEFAULT_COST)
.map_err(|e| ApiError::Internal(anyhow::anyhow!("Failed to hash OTP: {}", e)))?;
.map_err(|e| ApiError::Internal(anyhow::anyhow!("Failed to hash OTP: {e}")))?;

let expires_at = Utc::now() + Duration::minutes(5);

Expand Down Expand Up @@ -385,9 +385,13 @@ pub async fn send_2fa(
AuditLogService::log(
&state.db,
Some(payload.user_id),
"2fa_sent",
None,
audit_action::TWO_FA_SENT,
Some(payload.user_id),
Some("user"),
Some(entity_type::USER),
None,
None,
None,
)
.await?;

Expand Down Expand Up @@ -435,7 +439,7 @@ pub async fn verify_2fa_internal(db: &PgPool, user_id: Uuid, otp: &str) -> Resul

// 4. Verify OTP
let valid = bcrypt::verify(otp, &otp_hash)
.map_err(|e| ApiError::Internal(anyhow::anyhow!("Failed to verify OTP: {}", e)))?;
.map_err(|e| ApiError::Internal(anyhow::anyhow!("Failed to verify OTP: {e}")))?;

if !valid {
// Increment attempts
Expand Down
6 changes: 5 additions & 1 deletion backend/src/compliance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,13 @@ impl ComplianceEngine {
AuditLogService::log(
&mut *tx,
Some(user_id),
None,
audit_action::SUSPICIOUS_BORROWING_DETECTED,
Some(plan_id),
Some(entity_type::PLAN),
None,
None,
None,
)
.await?;

Expand All @@ -227,7 +231,7 @@ impl ComplianceEngine {
&mut tx,
user_id,
notif_type::SUSPICIOUS_ACTIVITY_FLAGGED,
format!("ALARM: Your account has been flagged for abnormal activity: {}. A compliance officer has been notified.", reason)
format!("ALARM: Your account has been flagged for abnormal activity: {reason}. A compliance officer has been notified.")
).await?;

tx.commit().await?;
Expand Down
12 changes: 12 additions & 0 deletions backend/src/emergency_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,13 @@ impl EmergencyAccessService {
AuditLogService::log(
&mut *tx,
Some(admin_id),
None,
audit_action::EMERGENCY_ACCESS_GRANTED,
Some(req.plan_id),
Some(entity_type::PLAN),
None,
None,
None,
)
.await?;

Expand Down Expand Up @@ -197,9 +201,13 @@ impl EmergencyAccessService {
AuditLogService::log(
&mut *tx,
Some(admin_id),
None,
audit_action::EMERGENCY_ACCESS_REVOKED,
Some(access.plan_id),
Some(entity_type::PLAN),
None,
None,
None,
)
.await?;

Expand Down Expand Up @@ -322,9 +330,13 @@ impl EmergencyAccessService {
if let Err(e) = AuditLogService::log(
&mut *tx,
None,
None,
audit_action::EMERGENCY_ACCESS_EXPIRED,
Some(plan_id),
Some(entity_type::PLAN),
None,
None,
None,
)
.await
{
Expand Down
5 changes: 2 additions & 3 deletions backend/src/event_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub async fn get_plan_events(
let plan =
crate::service::PlanService::get_plan_by_id(&state.db, plan_id, user.user_id).await?;
if plan.is_none() {
return Err(ApiError::NotFound(format!("Plan {} not found", plan_id)));
return Err(ApiError::NotFound(format!("Plan {plan_id} not found")));
}

let event_type = if let Some(et_str) = params.event_type {
Expand Down Expand Up @@ -131,8 +131,7 @@ fn parse_event_type(s: &str) -> Result<EventType, ApiError> {
"liquidation" => Ok(EventType::Liquidation),
"interest_accrual" => Ok(EventType::InterestAccrual),
_ => Err(ApiError::BadRequest(format!(
"Invalid event type: {}. Valid types: deposit, borrow, repay, liquidation, interest_accrual",
s
"Invalid event type: {s}. Valid types: deposit, borrow, repay, liquidation, interest_accrual"
))),
}
}
Expand Down
Loading