diff --git a/app.py b/app.py index f3d7f20c..cedbe8f3 100644 --- a/app.py +++ b/app.py @@ -33,7 +33,6 @@ from backend.utils.i18n import t from routes.irrigation_routes import irrigation_bp -app.register_blueprint(irrigation_bp) from server.Routes.rotation_routes import rotation_bp @@ -49,7 +48,6 @@ load_dotenv() app = Flask(__name__, static_folder='.', static_url_path='') -app.register_blueprint(rotation_bp) # Load Configuration env_name = os.getenv('FLASK_ENV', 'default') @@ -82,6 +80,8 @@ app.register_blueprint(files_bp) app.register_blueprint(spatial_bp) app.register_blueprint(ingestion_bp, url_prefix='/api/v1') +app.register_blueprint(irrigation_bp) +app.register_blueprint(rotation_bp) # Register API v1 (including loan, weather, schemes, etc.) register_api(app) @@ -508,7 +508,6 @@ def contact(): return send_from_directory('.', 'contact.html') @app.route('/chat') -@limiter.limit("10 per minute") def chat(): return send_from_directory('.', 'chat.html') diff --git a/backend/api/v1/farm_members.py b/backend/api/v1/farm_members.py index b8e515f7..ab5ec6a4 100644 --- a/backend/api/v1/farm_members.py +++ b/backend/api/v1/farm_members.py @@ -1,5 +1,6 @@ from flask import Blueprint, request, jsonify from backend.services.farm_service import FarmService +from backend.services.audit_service import AuditService from backend.models.farm import FarmMember, FarmRole from auth_utils import token_required @@ -38,6 +39,14 @@ def invite_member(current_user, farm_id): if error: return jsonify({'status': 'error', 'message': error}), 400 + AuditService.log_action( + action="INVITE_FARM_MEMBER", + user_id=current_user.id, + resource_type="FARM_MEMBER", + resource_id=str(member.id), + meta_data={"farm_id": farm_id, "invited_user_id": user_id, "role": role} + ) + return jsonify({ 'status': 'success', 'data': member.to_dict() @@ -58,6 +67,15 @@ def remove_member(current_user, member_id): if member.role == FarmRole.OWNER.value: return jsonify({'status': 'error', 'message': 'Cannot remove farm owner'}), 400 + AuditService.log_action( + action="REMOVE_FARM_MEMBER", + user_id=current_user.id, + resource_type="FARM_MEMBER", + resource_id=str(member_id), + risk_level='MEDIUM', + meta_data={"farm_id": member.farm_id, "removed_user_id": member.user_id} + ) + db.session.delete(member) db.session.commit() diff --git a/backend/api/v1/farms.py b/backend/api/v1/farms.py index cd03e49d..1eab3682 100644 --- a/backend/api/v1/farms.py +++ b/backend/api/v1/farms.py @@ -1,6 +1,7 @@ from flask import Blueprint, request, jsonify from backend.services.farm_service import FarmService from backend.services.farm_analytics import FarmAnalytics +from backend.services.audit_service import AuditService from auth_utils import token_required import logging @@ -36,6 +37,14 @@ def create_farm(current_user): if error: return jsonify({'status': 'error', 'message': error}), 500 + AuditService.log_action( + action="CREATE_FARM", + user_id=current_user.id, + resource_type="FARM", + resource_id=str(farm.id), + meta_data={"name": data['name'], "location": data['location']} + ) + return jsonify({ 'status': 'success', 'data': farm.to_dict() @@ -72,6 +81,14 @@ def add_asset(current_user, farm_id): if error: return jsonify({'status': 'error', 'message': error}), 500 + AuditService.log_action( + action="ADD_FARM_ASSET", + user_id=current_user.id, + resource_type="FARM_ASSET", + resource_id=str(asset.id), + meta_data={"name": data['name'], "category": data['category'], "farm_id": farm_id} + ) + return jsonify({ 'status': 'success', 'data': asset.to_dict() diff --git a/backend/api/v1/insurance.py b/backend/api/v1/insurance.py index f4720f2c..126cc7fc 100644 --- a/backend/api/v1/insurance.py +++ b/backend/api/v1/insurance.py @@ -7,6 +7,7 @@ from marshmallow import Schema, fields, validate, ValidationError from backend.services.insurance_service import InsuranceService +from backend.services.audit_service import AuditService from backend.auth_utils import token_required @@ -76,6 +77,14 @@ def create_policy(current_user): coverage_months=data['coverage_months'] ) + AuditService.log_action( + action="CREATE_INSURANCE_POLICY", + user_id=current_user.id, + resource_type="INSURANCE_POLICY", + resource_id=str(policy['id']), + meta_data=data + ) + return jsonify({ 'success': True, 'data': policy @@ -197,6 +206,14 @@ def renew_policy(current_user, policy_id): adjust_coverage=data.get('adjust_coverage') ) + AuditService.log_action( + action="RENEW_INSURANCE_POLICY", + user_id=current_user.id, + resource_type="INSURANCE_POLICY", + resource_id=str(policy_id), + meta_data=data + ) + return jsonify({ 'success': True, 'data': renewed_policy @@ -247,6 +264,15 @@ def cancel_policy(current_user, policy_id): result = InsuranceService.cancel_policy(policy_id, reason) + AuditService.log_action( + action="CANCEL_INSURANCE_POLICY", + user_id=current_user.id, + resource_type="INSURANCE_POLICY", + resource_id=str(policy_id), + risk_level='MEDIUM', + meta_data={"reason": reason} + ) + return jsonify({ 'success': True, 'data': result @@ -303,6 +329,15 @@ def submit_claim(current_user): evidence_photos=data['evidence_photos'] ) + AuditService.log_action( + action="SUBMIT_INSURANCE_CLAIM", + user_id=current_user.id, + resource_type="INSURANCE_CLAIM", + resource_id=str(claim['id']), + risk_level='MEDIUM', + meta_data=data + ) + return jsonify({ 'success': True, 'data': claim @@ -411,6 +446,15 @@ def process_claim(current_user, claim_id): processed_by=current_user.id ) + AuditService.log_action( + action="PROCESS_INSURANCE_CLAIM", + user_id=current_user.id, + resource_type="INSURANCE_CLAIM", + resource_id=str(claim_id), + risk_level='HIGH' if data['decision'] == 'APPROVED' else 'MEDIUM', + meta_data=data + ) + return jsonify({ 'success': True, 'data': result @@ -454,6 +498,15 @@ def mark_as_paid(current_user, claim_id): result = InsuranceService.mark_claim_paid(claim_id, payment_ref) + AuditService.log_action( + action="PAY_INSURANCE_CLAIM", + user_id=current_user.id, + resource_type="INSURANCE_CLAIM", + resource_id=str(claim_id), + risk_level='HIGH', + meta_data={"payment_reference": payment_ref} + ) + return jsonify({ 'success': True, 'data': result diff --git a/backend/api/v1/procurement.py b/backend/api/v1/procurement.py index f74f14f8..8036aff9 100644 --- a/backend/api/v1/procurement.py +++ b/backend/api/v1/procurement.py @@ -1,5 +1,6 @@ from flask import Blueprint, request, jsonify from backend.services.procurement_service import ProcurementService +from backend.services.audit_service import AuditService from backend.models.procurement import ProcurementItem, BulkOrder from auth_utils import token_required @@ -43,6 +44,14 @@ def place_order(current_user): if error: return jsonify({'status': 'error', 'message': error}), 500 + AuditService.log_action( + action="PLACE_PROCUREMENT_ORDER", + user_id=current_user.id, + resource_type="BULK_ORDER", + resource_id=str(order.id), + meta_data={"item_id": data['item_id'], "quantity": data['quantity'], "total": float(order.total_amount)} + ) + return jsonify({ 'status': 'success', 'data': {'order_id': order.id, 'total_amount': order.total_amount} diff --git a/backend/api/v1/warehouse.py b/backend/api/v1/warehouse.py index 83ef42fd..b391897f 100644 --- a/backend/api/v1/warehouse.py +++ b/backend/api/v1/warehouse.py @@ -1,5 +1,6 @@ from flask import Blueprint, request, jsonify from backend.services.inventory_service import InventoryService +from backend.services.audit_service import AuditService from backend.models.warehouse import StockItem, StockMovement, WarehouseLocation from auth_utils import token_required @@ -19,7 +20,7 @@ def list_stock(current_user): 'data': [i.to_dict() for i in items] }), 200 -@warehouse_bp.route('/stock/in', methods='POST']) +@warehouse_bp.route('/stock/in', methods=['POST']) @token_required def stock_in(current_user): """Records incoming stock.""" @@ -34,6 +35,14 @@ def stock_in(current_user): if error: return jsonify({'status': 'error', 'message': error}), 500 + AuditService.log_action( + action="STOCK_IN", + user_id=current_user.id, + resource_type="STOCK_ITEM", + resource_id=str(data['stock_item_id']), + meta_data={"quantity": data['quantity'], "reference": data.get('reference')} + ) + return jsonify({'status': 'success', 'message': 'Stock received'}), 201 @warehouse_bp.route('/stock/out', methods=['POST']) @@ -51,6 +60,14 @@ def stock_out(current_user): if error: return jsonify({'status': 'error', 'message': error}), 400 + AuditService.log_action( + action="STOCK_OUT", + user_id=current_user.id, + resource_type="STOCK_ITEM", + resource_id=str(data['stock_item_id']), + meta_data={"quantity": data['quantity'], "reference": data.get('reference')} + ) + return jsonify({'status': 'success', 'message': 'Stock issued'}), 201 @warehouse_bp.route('/reconcile', methods=['POST']) @@ -67,6 +84,15 @@ def reconcile(current_user): if error: return jsonify({'status': 'error', 'message': error}), 500 + AuditService.log_action( + action="STOCK_RECONCILIATION", + user_id=current_user.id, + resource_type="WAREHOUSE", + resource_id=str(data['warehouse_id']), + risk_level='MEDIUM', + meta_data={"discrepancies": log.discrepancies_found, "shrinkage": float(log.shrinkage_value)} + ) + return jsonify({ 'status': 'success', 'data': { diff --git a/backend/models/__init__.py b/backend/models/__init__.py index c23d1ca9..781a5fd8 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -27,6 +27,8 @@ from .loan_v2 import RepaymentSchedule, PaymentHistory, DefaultRiskScore, CollectionNote from .warehouse import WarehouseLocation, StockItem, StockMovement, ReconciliationLog from .climate import ClimateZone, SensorNode, TelemetryLog, AutomationTrigger +from .labor import WorkerProfile, WorkShift, HarvestLog, PayrollEntry +from .logistics_v2 import DriverProfile, DeliveryVehicle, TransportRoute, FuelLog __all__ = [ 'User', 'UserRole', 'LoanRequest', 'PredictionHistory', diff --git a/requirements.txt b/requirements.txt index bc637bcd..8865208b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ Flask-Babel Flask-SQLAlchemy firebase-admin requests +python-magic bcrypt email-validator torch