Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,5 @@ Thumbs.db
*.pth
*.h5
*.onnx
ai_models/*.pkl
ai_models/*.png
250 changes: 126 additions & 124 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
from datetime import datetime
from typing import Any, Dict, Optional, Tuple
from werkzeug.utils import secure_filename
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from io import BytesIO
from services.weather_service import get_weather
from sqlalchemy import inspect, text
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from io import BytesIO
from services.weather_service import get_weather
from sqlalchemy import inspect, text

import redis
import base64
Expand Down Expand Up @@ -48,15 +48,15 @@
from ultralytics import YOLO
import json
from jinja2 import Environment, FileSystemLoader
from model_registry import registry
from services.weather_service import generate_weather_recommendations
from services.yield_service import estimate_yield
from services.auth_security_service import (
AccountLockoutService,
get_client_ip,
get_user_agent,
)
from security_utils import (
from model_registry import registry
from services.weather_service import generate_weather_recommendations
from services.yield_service import estimate_yield
from services.auth_security_service import (
AccountLockoutService,
get_client_ip,
get_user_agent,
)
from security_utils import (
UploadValidationError,
cleanup_temp_upload,
resolve_secret_key,
Expand Down Expand Up @@ -102,57 +102,57 @@
storage_uri=limiter_storage_uri,
strategy="fixed-window",
)
from models import db
db.init_app(app)


_account_lockout_schema_checked = False


def ensure_account_lockout_schema() -> None:
"""Backfill account lockout columns for existing create_all-managed DBs."""
inspector = inspect(db.engine)
if "users" not in inspector.get_table_names():
return

existing_columns = {column["name"] for column in inspector.get_columns("users")}
dialect = db.engine.dialect.name
datetime_type = "TIMESTAMP" if dialect == "postgresql" else "DATETIME"
columns = {
"failed_login_attempts": "INTEGER NOT NULL DEFAULT 0",
"last_failed_login_at": datetime_type,
"account_locked_until": datetime_type,
"last_successful_login_at": datetime_type,
"last_failed_ip": "VARCHAR(64)",
"last_successful_ip": "VARCHAR(64)",
}

changed = False
with db.engine.begin() as connection:
for column_name, ddl_type in columns.items():
if column_name not in existing_columns:
connection.execute(text(f"ALTER TABLE users ADD COLUMN {column_name} {ddl_type}"))
changed = True
connection.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_users_account_locked_until "
"ON users (account_locked_until)"
)
)
if changed:
logger.info("Account lockout schema columns added to users table")


@app.before_request
def _ensure_account_lockout_schema_once() -> None:
global _account_lockout_schema_checked
if _account_lockout_schema_checked or app.config.get("TESTING"):
return
try:
ensure_account_lockout_schema()
_account_lockout_schema_checked = True
except Exception as exc:
logger.warning("Account lockout schema check skipped: %s", exc)
from models import db
db.init_app(app)
_account_lockout_schema_checked = False
def ensure_account_lockout_schema() -> None:
"""Backfill account lockout columns for existing create_all-managed DBs."""
inspector = inspect(db.engine)
if "users" not in inspector.get_table_names():
return
existing_columns = {column["name"] for column in inspector.get_columns("users")}
dialect = db.engine.dialect.name
datetime_type = "TIMESTAMP" if dialect == "postgresql" else "DATETIME"
columns = {
"failed_login_attempts": "INTEGER NOT NULL DEFAULT 0",
"last_failed_login_at": datetime_type,
"account_locked_until": datetime_type,
"last_successful_login_at": datetime_type,
"last_failed_ip": "VARCHAR(64)",
"last_successful_ip": "VARCHAR(64)",
}
changed = False
with db.engine.begin() as connection:
for column_name, ddl_type in columns.items():
if column_name not in existing_columns:
connection.execute(text(f"ALTER TABLE users ADD COLUMN {column_name} {ddl_type}"))
changed = True
connection.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_users_account_locked_until "
"ON users (account_locked_until)"
)
)
if changed:
logger.info("Account lockout schema columns added to users table")
@app.before_request
def _ensure_account_lockout_schema_once() -> None:
global _account_lockout_schema_checked
if _account_lockout_schema_checked or app.config.get("TESTING"):
return
try:
ensure_account_lockout_schema()
_account_lockout_schema_checked = True
except Exception as exc:
logger.warning("Account lockout schema check skipped: %s", exc)

# --- Login Manager Configuration ---
login_manager = LoginManager()
Expand Down Expand Up @@ -900,7 +900,7 @@ def generate_gradcam_explanation(
return grad_cam_image_b64, heatmap_only_b64


def analyze_image(image: np.ndarray,*,weather:Optional[dict]=None,field_acres: float=1.0) -> Dict[str, Any]:
def analyze_image(image: np.ndarray,*,weather:Optional[dict]=None,field_acres: float=1.0, crop_type: str="cotton") -> Dict[str, Any]:
import time
start_time = time.time()
field_acres=normalize_field_acres(field_acres)
Expand Down Expand Up @@ -990,7 +990,7 @@ def analyze_image(image: np.ndarray,*,weather:Optional[dict]=None,field_acres: f

recs = generate_recommendations(disease, growth,weather=weather)
severity = calculate_disease_severity(disease["health_score"])
yield_est = estimate_yield(disease, growth, weather=weather, field_acres=field_acres)
yield_est = estimate_yield(disease, growth, weather=weather, field_acres=field_acres, crop_type=crop_type)
adv_recs = generate_advanced_recommendations(disease, growth)
treatment_recs = generate_treatment_recommendations(disease)
insights = generate_farmer_insights(disease, growth)
Expand Down Expand Up @@ -1868,10 +1868,11 @@ def analyze():
lon = request.form.get("lon", type=float)
city = request.form.get("city", type=str)
field_acres=normalize_field_acres(request.form.get("field_acres"))
crop_type = request.form.get("crop_type", "cotton").lower().strip()

weather=resolve_weather_for_analysis(lat=lat,lon=lon,city=city)

results = analyze_image(compressed_rgb,weather=weather,field_acres=field_acres)
results = analyze_image(compressed_rgb,weather=weather,field_acres=field_acres,crop_type=crop_type)

if results.get("error"):
raise ValueError(results["error"])
Expand Down Expand Up @@ -2212,7 +2213,7 @@ def demo():

# Use estimate_yield from service
from services.yield_service import estimate_yield
yield_est = estimate_yield(demo_disease, demo_growth, weather=None, field_acres=1.0)
yield_est = estimate_yield(demo_disease, demo_growth, weather=None, field_acres=1.0, crop_type="cotton")

# Generate advanced recommendations
adv_recs = generate_advanced_recommendations(demo_disease, demo_growth)
Expand Down Expand Up @@ -2371,7 +2372,8 @@ def api_analyze():
field_acres,field_acres_error=parse_api_field_acres(request.form.get("field_acres"))
if field_acres_error:
return jsonify({"error":field_acres_error}),400

raw_crop = request.form.get("crop_type", "cotton").lower().strip()
crop_type = raw_crop if raw_crop in ("cotton", "tomato", "potato") else "cotton"
lat=request.form.get("lat",type=float)
lon=request.form.get("lon",type=float)
city=request.form.get("city",type=str)
Expand Down Expand Up @@ -2839,58 +2841,58 @@ def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

if request.method == 'POST':
email = (request.form.get('email') or '').strip().lower()
password = request.form.get('password') or ''
remember = request.form.get('remember')
ip_address = get_client_ip()
user_agent = get_user_agent()
lockout_service = AccountLockoutService()

from models import User
user = User.query.filter_by(email=email).first()

if user:
lockout_state = lockout_service.check_lockout(user)
if lockout_state.unlocked_expired_lock:
lockout_service.record_unlock(
user,
ip=ip_address,
user_agent=user_agent,
)
db.session.commit()
if lockout_state.locked:
flash('Account temporarily locked. Please try again later.', 'danger')
return render_template(
'login.html',
google_oauth_enabled=GOOGLE_OAUTH_ENABLED,
), 423

if user and user.check_password(password):
if not user.is_active:
flash('Your account has been deactivated. Please contact support.', 'danger')
return render_template('login.html', google_oauth_enabled=GOOGLE_OAUTH_ENABLED)

login_user(user, remember=remember)
lockout_service.record_successful_login(
user,
ip=ip_address,
user_agent=user_agent,
)
user.last_login = user.last_successful_login_at
db.session.commit()
if request.method == 'POST':
email = (request.form.get('email') or '').strip().lower()
password = request.form.get('password') or ''
remember = request.form.get('remember')
ip_address = get_client_ip()
user_agent = get_user_agent()
lockout_service = AccountLockoutService()
from models import User
user = User.query.filter_by(email=email).first()
if user:
lockout_state = lockout_service.check_lockout(user)
if lockout_state.unlocked_expired_lock:
lockout_service.record_unlock(
user,
ip=ip_address,
user_agent=user_agent,
)
db.session.commit()
if lockout_state.locked:
flash('Account temporarily locked. Please try again later.', 'danger')
return render_template(
'login.html',
google_oauth_enabled=GOOGLE_OAUTH_ENABLED,
), 423
if user and user.check_password(password):
if not user.is_active:
flash('Your account has been deactivated. Please contact support.', 'danger')
return render_template('login.html', google_oauth_enabled=GOOGLE_OAUTH_ENABLED)
login_user(user, remember=remember)
lockout_service.record_successful_login(
user,
ip=ip_address,
user_agent=user_agent,
)
user.last_login = user.last_successful_login_at
db.session.commit()

next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('index'))
else:
if user:
lockout_service.record_failed_login(
user,
ip=ip_address,
user_agent=user_agent,
)
db.session.commit()
flash('Invalid email or password', 'danger')
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('index'))
else:
if user:
lockout_service.record_failed_login(
user,
ip=ip_address,
user_agent=user_agent,
)
db.session.commit()
flash('Invalid email or password', 'danger')

return render_template('login.html', google_oauth_enabled=GOOGLE_OAUTH_ENABLED)

Expand Down Expand Up @@ -3724,9 +3726,9 @@ def analyze_result():

# Initialize database tables
with app.app_context():
db.create_all()
ensure_account_lockout_schema()
logger.info("Database tables created")
db.create_all()
ensure_account_lockout_schema()
logger.info("Database tables created")

# Seed enterprise RBAC (idempotent)
try:
Expand Down
Loading
Loading