nordabiz/app.py
Maciej Pienczyn 5bd1b149c7
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat(ai): Add Gemini model fallback chain for 429 rate limit resilience
Switch primary model to flash-lite (1000 RPD) with automatic fallback
to 3-flash-preview (20 RPD) and flash (20 RPD) on RESOURCE_EXHAUSTED,
giving 1040 req/day on free tier instead of 20.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:35:22 +01:00

1558 lines
56 KiB
Python

#!/usr/bin/env python3
"""
Norda Biznes Partner - Flask Application
====================================
Main Flask application for Norda Biznes company directory with AI chat.
Features:
- User authentication with email confirmation
- Company directory with advanced search
- AI chat assistant powered by Google Gemini
- PostgreSQL database integration
- Analytics dashboard for chat insights
Author: Norda Biznes Development Team
Created: 2025-11-23
"""
import os
import logging
import secrets
import re
import json
import time
from collections import deque
from pathlib import Path
from datetime import datetime, timedelta, date
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, Response, send_file
from flask_login import login_user, logout_user, login_required, current_user
# Note: CSRFProtect, Limiter, LoginManager are imported from extensions.py (line ~250)
from werkzeug.security import generate_password_hash, check_password_hash
from dotenv import load_dotenv
from user_agents import parse as parse_user_agent
import uuid
import traceback as tb_module
# Load environment variables (override any existing env vars)
# Try .env first, then nordabiz_config.txt for production flexibility
import os
if os.path.exists('.env'):
load_dotenv('.env', override=True)
elif os.path.exists('nordabiz_config.txt'):
load_dotenv('nordabiz_config.txt', override=True)
else:
load_dotenv(override=True)
# ============================================================
# GLOBAL CONSTANTS - MARKETING
# ============================================================
# Liczba podmiotów gospodarczych (cel marketingowy Izby NORDA)
# Używana we wszystkich miejscach wyświetlających liczbę firm
COMPANY_COUNT_MARKETING = 150
# ============================================================
# STAGING TEST FEATURES
# ============================================================
# Features currently being tested on staging environment.
# Only rendered when STAGING=true in .env. Edit this dict to update.
STAGING_TEST_FEATURES = {
'board_module': {
'name': 'Moduł Rada Izby',
'description': 'Zarządzanie posiedzeniami, dokumenty, rola OFFICE_MANAGER',
'nav_item': 'Rada',
},
}
# Configure logging with in-memory buffer for debug panel
class DebugLogHandler(logging.Handler):
"""Custom handler that stores logs in memory for real-time viewing"""
def __init__(self, max_logs=500):
super().__init__()
self.logs = deque(maxlen=max_logs)
def emit(self, record):
log_entry = {
'timestamp': datetime.now().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': self.format(record),
'module': record.module,
'funcName': record.funcName,
'lineno': record.lineno
}
self.logs.append(log_entry)
# Create debug handler
debug_handler = DebugLogHandler(max_logs=500)
debug_handler.setFormatter(logging.Formatter('%(message)s'))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Add debug handler to root logger
logging.getLogger().addHandler(debug_handler)
logger = logging.getLogger(__name__)
# Security logger for fail2ban integration
# Logs to /var/log/nordabiznes/security.log in production
security_logger = logging.getLogger('security')
security_logger.setLevel(logging.WARNING)
_security_log_path = '/var/log/nordabiznes/security.log'
if os.path.exists('/var/log/nordabiznes'):
_security_handler = logging.FileHandler(_security_log_path)
_security_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
security_logger.addHandler(_security_handler)
# Import database models
from database import (
init_db,
SessionLocal,
User,
Company,
Category,
Service,
Competency,
CompanyDigitalMaturity,
CompanyWebsiteAnalysis,
CompanyQualityTracking,
CompanyWebsiteContent,
CompanyAIInsights,
CompanyEvent,
CompanySocialMedia,
CompanyContact,
AIChatConversation,
AIChatMessage,
AIChatFeedback,
AIAPICostLog,
ForumTopic,
ForumReply,
ForumAttachment,
NordaEvent,
EventAttendee,
PrivateMessage,
Classified,
UserNotification,
CompanyRecommendation,
MembershipFee,
MembershipFeeConfig,
Person,
CompanyPerson,
GBPAudit,
ITAudit,
KRSAudit,
CompanyPKD,
CompanyFinancialReport,
UserSession,
UserBlock,
PageView,
UserClick,
AnalyticsDaily,
PopularPagesDaily,
SearchQuery,
ConversionEvent,
JSError,
PopularSearchesDaily,
HourlyActivity,
AuditLog,
SecurityAlert,
ZOPKNews,
SystemRole
)
from utils.decorators import role_required
# Import services
import gemini_service
from nordabiz_chat import NordaBizChatEngine
from search_service import search_companies
import krs_api_service
from file_upload_service import FileUploadService
# Security service for audit log, alerting, GeoIP, 2FA
try:
from security_service import (
log_audit, create_security_alert, get_client_ip,
is_ip_allowed, geoip_check, init_security_service,
generate_totp_secret, get_totp_uri, verify_totp,
generate_backup_codes, verify_backup_code, requires_2fa
)
SECURITY_SERVICE_AVAILABLE = True
except ImportError as e:
SECURITY_SERVICE_AVAILABLE = False
logger.warning(f"Security service not available: {e}")
# News service for fetching company news
try:
from news_service import NewsService, get_news_service, init_news_service
NEWS_SERVICE_AVAILABLE = True
except ImportError:
NEWS_SERVICE_AVAILABLE = False
logger.warning("News service not available")
# SEO audit components for triggering audits via API
import sys
_scripts_path = os.path.join(os.path.dirname(__file__), 'scripts')
if _scripts_path not in sys.path:
sys.path.insert(0, _scripts_path)
try:
from seo_audit import SEOAuditor, SEO_AUDIT_VERSION
SEO_AUDIT_AVAILABLE = True
except ImportError as e:
SEO_AUDIT_AVAILABLE = False
logger.warning(f"SEO audit service not available: {e}")
# GBP (Google Business Profile) audit service
try:
from gbp_audit_service import (
GBPAuditService,
audit_company as gbp_audit_company,
get_company_audit as gbp_get_company_audit,
fetch_google_business_data as gbp_fetch_google_data
)
GBP_AUDIT_AVAILABLE = True
GBP_AUDIT_VERSION = '1.0'
except ImportError as e:
GBP_AUDIT_AVAILABLE = False
GBP_AUDIT_VERSION = None
logger.warning(f"GBP audit service not available: {e}")
# KRS (Krajowy Rejestr Sądowy) audit service
try:
from krs_audit_service import parse_krs_pdf, parse_krs_pdf_full
KRS_AUDIT_AVAILABLE = True
KRS_AUDIT_VERSION = '1.0'
except ImportError as e:
KRS_AUDIT_AVAILABLE = False
KRS_AUDIT_VERSION = None
logger.warning(f"KRS audit service not available: {e}")
# Initialize Flask app
app = Flask(__name__)
# Security: Require strong SECRET_KEY (no default value allowed)
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY or len(SECRET_KEY) < 32:
raise ValueError("SECRET_KEY must be set in environment variables and be at least 32 characters long")
app.config['SECRET_KEY'] = SECRET_KEY
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
# Security configurations
app.config['WTF_CSRF_ENABLED'] = True
app.config['WTF_CSRF_TIME_LIMIT'] = None # No time limit for CSRF tokens
app.config['SESSION_COOKIE_SECURE'] = os.getenv('FLASK_ENV') != 'development' # HTTPS only in production
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Template filters
@app.template_filter('ensure_url')
def ensure_url_filter(url):
"""Ensure URL has http:// or https:// scheme"""
if url and not url.startswith(('http://', 'https://')):
return f'https://{url}'
return url
# Register forum markdown filter
from utils.markdown import register_markdown_filter
register_markdown_filter(app)
# Initialize extensions from centralized extensions.py
from extensions import csrf, limiter, login_manager
csrf.init_app(app)
# Initialize rate limiter with Redis storage (persistent across restarts)
# Falls back to memory if Redis unavailable
_redis_available = False
try:
import redis
_redis_client = redis.Redis(host='localhost', port=6379, db=0)
_redis_client.ping()
_redis_available = True
logger.info("Rate limiter using Redis storage")
except Exception:
logger.warning("Redis unavailable, rate limiter using memory storage")
# Note: default_limits are set in extensions.py
# Here we only configure storage
if _redis_available:
limiter._storage_uri = "redis://localhost:6379/0"
else:
limiter._storage_uri = "memory://"
limiter.init_app(app)
@limiter.request_filter
def is_admin_exempt():
"""Exempt logged-in admins from rate limiting."""
from flask_login import current_user
try:
return current_user.is_authenticated and current_user.has_role(SystemRole.ADMIN)
except Exception:
return False
# Initialize database
init_db()
# Initialize Login Manager (imported from extensions.py)
login_manager.init_app(app)
login_manager.login_view = 'login' # Will change to 'auth.login' after full migration
login_manager.login_message = 'Zaloguj się, aby uzyskać dostęp do tej strony.'
# Initialize Gemini service
try:
gemini_service.init_gemini_service(model='flash-lite') # Primary: 1000 RPD, fallback: 3-flash (20 RPD) → flash (20 RPD)
logger.info("Gemini service initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Gemini service: {e}")
# Register blueprints (Phase 1: reports, community)
from blueprints import register_blueprints
register_blueprints(app)
logger.info("Blueprints registered")
@login_manager.user_loader
def load_user(user_id):
"""Load user from database"""
db = SessionLocal()
try:
return db.query(User).filter_by(id=int(user_id)).first()
finally:
db.close()
# ============================================================
# TEMPLATE CONTEXT PROCESSORS
# ============================================================
@app.context_processor
def inject_globals():
"""Inject global variables into all templates"""
is_staging = os.getenv('STAGING') == 'true'
return {
'current_year': datetime.now().year,
'now': datetime.now(), # Must be value, not method - templates use now.strftime()
'COMPANY_COUNT': COMPANY_COUNT_MARKETING, # Liczba podmiotów (cel marketingowy)
'is_staging': is_staging,
'staging_features': STAGING_TEST_FEATURES if is_staging else {},
'SystemRole': SystemRole,
}
@app.context_processor
def inject_notifications():
"""Inject unread notifications count into all templates"""
if current_user.is_authenticated:
db = SessionLocal()
try:
unread_count = db.query(UserNotification).filter(
UserNotification.user_id == current_user.id,
UserNotification.is_read == False
).count()
return {'unread_notifications_count': unread_count}
finally:
db.close()
return {'unread_notifications_count': 0}
# ============================================================
# NOTIFICATION HELPERS
# ============================================================
def create_notification(user_id, title, message, notification_type='info',
related_type=None, related_id=None, action_url=None):
"""
Create a notification for a user.
Args:
user_id: ID of the user to notify
title: Notification title
message: Notification message/body
notification_type: Type of notification (news, system, message, event, alert)
related_type: Type of related entity (company_news, event, message, etc.)
related_id: ID of the related entity
action_url: URL to navigate when notification is clicked
Returns:
UserNotification object or None on error
"""
db = SessionLocal()
try:
notification = UserNotification(
user_id=user_id,
title=title,
message=message,
notification_type=notification_type,
related_type=related_type,
related_id=related_id,
action_url=action_url
)
db.add(notification)
db.commit()
db.refresh(notification)
logger.info(f"Created notification for user {user_id}: {title}")
return notification
except Exception as e:
logger.error(f"Error creating notification: {e}")
db.rollback()
return None
finally:
db.close()
def create_news_notification(company_id, news_id, news_title):
"""
Create notification for company owner when their news is approved.
Args:
company_id: ID of the company
news_id: ID of the approved news
news_title: Title of the news
"""
db = SessionLocal()
try:
# Find users associated with this company
users = db.query(User).filter(
User.company_id == company_id,
User.is_active == True
).all()
for user in users:
create_notification(
user_id=user.id,
title="Nowa aktualnosc o Twojej firmie",
message=f"Aktualnosc '{news_title}' zostala zatwierdzona i jest widoczna na profilu firmy.",
notification_type='news',
related_type='company_news',
related_id=news_id,
action_url=f"/company/{company_id}"
)
finally:
db.close()
# ============================================================
# USER ANALYTICS - TRACKING HELPERS
# ============================================================
# Global variable to store current page_view_id for templates
_current_page_view_id = {}
def get_or_create_analytics_session():
"""
Get existing analytics session or create new one.
Returns the database session ID (integer).
Includes GeoIP lookup and UTM parameter parsing.
"""
analytics_session_id = session.get('analytics_session_id')
if not analytics_session_id:
analytics_session_id = str(uuid.uuid4())
session['analytics_session_id'] = analytics_session_id
db = SessionLocal()
try:
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
if not user_session:
# Parse user agent
ua_string = request.headers.get('User-Agent', '')
try:
ua = parse_user_agent(ua_string)
device_type = 'mobile' if ua.is_mobile else ('tablet' if ua.is_tablet else 'desktop')
browser = ua.browser.family
browser_version = ua.browser.version_string
os_name = ua.os.family
os_version = ua.os.version_string
except Exception:
device_type = 'desktop'
browser = 'Unknown'
browser_version = ''
os_name = 'Unknown'
os_version = ''
# GeoIP lookup
country, city, region = None, None, None
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
if ip_address:
ip_address = ip_address.split(',')[0].strip()
try:
from security_service import get_geoip_info
geo_info = get_geoip_info(ip_address)
if geo_info:
country = geo_info.get('country')
city = geo_info.get('city')
region = geo_info.get('region')
except Exception as e:
logger.debug(f"GeoIP lookup failed for {ip_address}: {e}")
# UTM parameters (z pierwszego requestu sesji)
utm_source = request.args.get('utm_source', '')[:255] or None
utm_medium = request.args.get('utm_medium', '')[:255] or None
utm_campaign = request.args.get('utm_campaign', '')[:255] or None
utm_term = request.args.get('utm_term', '')[:255] or None
utm_content = request.args.get('utm_content', '')[:255] or None
user_session = UserSession(
session_id=analytics_session_id,
user_id=current_user.id if current_user.is_authenticated else None,
ip_address=ip_address,
user_agent=ua_string[:2000] if ua_string else None,
device_type=device_type,
browser=browser[:50] if browser else None,
browser_version=browser_version[:20] if browser_version else None,
os=os_name[:50] if os_name else None,
os_version=os_version[:20] if os_version else None,
# GeoIP
country=country,
city=city,
region=region,
# UTM
utm_source=utm_source,
utm_medium=utm_medium,
utm_campaign=utm_campaign,
utm_term=utm_term,
utm_content=utm_content
)
db.add(user_session)
db.commit()
db.refresh(user_session)
else:
# Update last activity AND duration
user_session.last_activity_at = datetime.now()
user_session.duration_seconds = int(
(datetime.now() - user_session.started_at).total_seconds()
)
if current_user.is_authenticated and not user_session.user_id:
user_session.user_id = current_user.id
db.commit()
return user_session.id
except Exception as e:
logger.error(f"Analytics session error: {e}")
db.rollback()
return None
finally:
db.close()
def track_conversion(event_type: str, company_id: int = None, target_type: str = None,
target_value: str = None, metadata: dict = None):
"""
Track conversion event.
Args:
event_type: Type of conversion (register, login, contact_click, rsvp, message, classified)
company_id: Related company ID (for contact_click)
target_type: What was clicked (email, phone, website)
target_value: The value (email address, phone number, etc.)
metadata: Additional data as dict
"""
try:
analytics_session_id = session.get('analytics_session_id')
session_db_id = None
db = SessionLocal()
try:
if analytics_session_id:
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
if user_session:
session_db_id = user_session.id
# Określ kategorię konwersji
category_map = {
'register': 'acquisition',
'login': 'activation',
'contact_click': 'engagement',
'rsvp': 'engagement',
'message': 'engagement',
'classified': 'engagement'
}
conversion = ConversionEvent(
session_id=session_db_id,
user_id=current_user.id if current_user.is_authenticated else None,
event_type=event_type,
event_category=category_map.get(event_type, 'other'),
company_id=company_id,
target_type=target_type,
target_value=target_value[:500] if target_value else None,
source_page=request.url[:500] if request.url else None,
referrer=request.referrer[:500] if request.referrer else None,
event_metadata=metadata
)
db.add(conversion)
db.commit()
logger.info(f"Conversion tracked: {event_type} company={company_id} target={target_type}")
except Exception as e:
logger.error(f"Conversion tracking error: {e}")
db.rollback()
finally:
db.close()
except Exception as e:
logger.error(f"Conversion tracking outer error: {e}")
@app.before_request
def check_geoip():
"""Block requests from high-risk countries (RU, CN, KP, IR, BY, SY, VE, CU)."""
# Skip static files and health checks
if request.path.startswith('/static') or request.path == '/health':
return
if not is_ip_allowed():
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if ip:
ip = ip.split(',')[0].strip()
from security_service import get_country_code
country = get_country_code(ip)
logger.warning(f"GEOIP_BLOCKED ip={ip} country={country} path={request.path}")
# Create alert for blocked access
try:
db = SessionLocal()
from security_service import create_security_alert
create_security_alert(
db, 'geo_blocked', 'low',
ip_address=ip,
details={'country': country, 'path': request.path, 'user_agent': request.user_agent.string[:200]}
)
db.commit()
db.close()
except Exception as e:
logger.error(f"Failed to create geo block alert: {e}")
abort(403)
@app.before_request
def track_page_view():
"""Track page views (excluding static files and API calls)"""
# Skip static files
if request.path.startswith('/static'):
return
# Skip API calls except selected ones
if request.path.startswith('/api'):
return
# Skip analytics tracking endpoints
if request.path in ['/api/analytics/track', '/api/analytics/heartbeat']:
return
# Skip health checks
if request.path == '/health':
return
# Skip favicon
if request.path == '/favicon.ico':
return
try:
session_db_id = get_or_create_analytics_session()
if not session_db_id:
return
db = SessionLocal()
try:
page_view = PageView(
session_id=session_db_id,
user_id=current_user.id if current_user.is_authenticated else None,
url=request.url[:2000] if request.url else '',
path=request.path[:500] if request.path else '/',
referrer=request.referrer[:2000] if request.referrer else None
)
# Extract company_id from path if on company page
if request.path.startswith('/company/'):
try:
slug = request.path.split('/')[2].split('?')[0]
company = db.query(Company).filter_by(slug=slug).first()
if company:
page_view.company_id = company.id
except Exception:
pass
db.add(page_view)
# Update session page count
user_session = db.query(UserSession).filter_by(id=session_db_id).first()
if user_session:
user_session.page_views_count = (user_session.page_views_count or 0) + 1
db.commit()
# Store page_view_id for click tracking (in request context)
_current_page_view_id[id(request)] = page_view.id
except Exception as e:
logger.error(f"Page view tracking error: {e}")
db.rollback()
finally:
db.close()
except Exception as e:
logger.error(f"Page view tracking outer error: {e}")
@app.context_processor
def inject_page_view_id():
"""Inject page_view_id into all templates for JS tracking"""
page_view_id = _current_page_view_id.get(id(request), '')
return {'page_view_id': page_view_id}
@app.teardown_request
def cleanup_page_view_id(exception=None):
"""Clean up page_view_id from global dict after request"""
_current_page_view_id.pop(id(request), None)
# ============================================================
# SECURITY MIDDLEWARE & HELPERS
# ============================================================
@app.after_request
def set_security_headers(response):
"""Add security headers to all responses"""
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
# Content Security Policy
csp = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
"img-src 'self' data: https:; "
"font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com; "
"connect-src 'self'"
)
response.headers['Content-Security-Policy'] = csp
return response
def validate_email(email):
"""Validate email format"""
if not email or len(email) > 255:
return False
# RFC 5322 compliant email regex (simplified)
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def validate_password(password):
"""
Validate password strength
Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
"""
if not password or len(password) < 8:
return False, "Hasło musi mieć minimum 8 znaków"
if not re.search(r'[A-Z]', password):
return False, "Hasło musi zawierać przynajmniej jedną wielką literę"
if not re.search(r'[a-z]', password):
return False, "Hasło musi zawierać przynajmniej jedną małą literę"
if not re.search(r'\d', password):
return False, "Hasło musi zawierać przynajmniej jedną cyfrę"
return True, "OK"
def sanitize_input(text, max_length=1000):
"""Sanitize user input - remove potentially dangerous characters"""
if not text:
return ""
# Remove null bytes
text = text.replace('\x00', '')
# Trim to max length
text = text[:max_length]
# Strip whitespace
text = text.strip()
return text
def get_free_tier_usage():
"""
Get today's Gemini API usage for free tier tracking.
Returns:
Dict with requests_today and tokens_today
"""
from datetime import date
from sqlalchemy import func
db = SessionLocal()
try:
today = date.today()
result = db.query(
func.count(AIAPICostLog.id).label('requests'),
func.coalesce(func.sum(AIAPICostLog.total_tokens), 0).label('tokens')
).filter(
func.date(AIAPICostLog.timestamp) == today,
AIAPICostLog.api_provider == 'gemini'
).first()
return {
'requests_today': result.requests or 0,
'tokens_today': int(result.tokens or 0)
}
except Exception as e:
logger.warning(f"Failed to get free tier usage: {e}")
return {'requests_today': 0, 'tokens_today': 0}
finally:
db.close()
def get_brave_api_usage():
"""
Get Brave Search API usage for current month.
Brave free tier: 2000 requests/month
Returns:
Dict with usage stats and limits
"""
from datetime import date
from sqlalchemy import func, extract
db = SessionLocal()
try:
today = date.today()
current_month = today.month
current_year = today.year
# Monthly usage
monthly_result = db.query(
func.count(AIAPICostLog.id).label('requests')
).filter(
extract('month', AIAPICostLog.timestamp) == current_month,
extract('year', AIAPICostLog.timestamp) == current_year,
AIAPICostLog.api_provider == 'brave'
).first()
# Today's usage
daily_result = db.query(
func.count(AIAPICostLog.id).label('requests')
).filter(
func.date(AIAPICostLog.timestamp) == today,
AIAPICostLog.api_provider == 'brave'
).first()
monthly_used = monthly_result.requests or 0
daily_used = daily_result.requests or 0
monthly_limit = 2000 # Brave free tier
return {
'requests_today': daily_used,
'requests_this_month': monthly_used,
'monthly_limit': monthly_limit,
'remaining': max(0, monthly_limit - monthly_used),
'usage_percent': round((monthly_used / monthly_limit) * 100, 1) if monthly_limit > 0 else 0,
'tier': 'free',
'is_limit_reached': monthly_used >= monthly_limit
}
except Exception as e:
logger.warning(f"Failed to get Brave API usage: {e}")
return {
'requests_today': 0,
'requests_this_month': 0,
'monthly_limit': 2000,
'remaining': 2000,
'usage_percent': 0,
'tier': 'free',
'is_limit_reached': False
}
finally:
db.close()
def log_brave_api_call(user_id=None, feature='news_search', company_name=None):
"""
Log a Brave API call for usage tracking.
Args:
user_id: User who triggered the call (optional)
feature: Feature name (news_search, etc.)
company_name: Company being searched (for reference)
"""
db = SessionLocal()
try:
log_entry = AIAPICostLog(
api_provider='brave',
model_name='search_api',
feature=feature,
user_id=user_id,
input_tokens=0,
output_tokens=0,
total_tokens=0
)
db.add(log_entry)
db.commit()
logger.debug(f"Logged Brave API call: {feature} for {company_name}")
except Exception as e:
logger.error(f"Failed to log Brave API call: {e}")
db.rollback()
finally:
db.close()
# ============================================================
# HEALTH CHECK
# ============================================================
@app.route('/health')
def health():
"""Health check endpoint for monitoring"""
return {'status': 'ok'}, 200
@app.route('/test-error-500')
@login_required
def test_error_500():
"""Test endpoint to trigger 500 error for notification testing. Admin only."""
if not current_user.can_access_admin_panel():
flash('Brak uprawnień', 'error')
return redirect(url_for('index'))
# Intentionally raise an error to test error notification
raise Exception("TEST ERROR 500 - Celowy błąd testowy do sprawdzenia powiadomień email")
@app.route('/health/full')
@login_required
@role_required(SystemRole.ADMIN)
def health_full():
"""
Extended health check - verifies all critical endpoints.
Returns detailed status of each endpoint.
Access: /health/full
"""
results = []
all_ok = True
# List of ALL endpoints to check (path, name)
# Comprehensive list updated 2026-01-17
endpoints = [
# ========== PUBLIC PAGES ==========
('/', 'Strona główna'),
('/login', 'Logowanie'),
('/register', 'Rejestracja'),
('/release-notes', 'Historia zmian'),
('/search?q=test', 'Wyszukiwarka'),
('/aktualnosci', 'Aktualności'),
('/forum', 'Forum'),
('/kalendarz', 'Kalendarz wydarzeń'),
('/tablica', 'Tablica ogłoszeń'),
('/nowi-czlonkowie', 'Nowi członkowie'),
('/mapa-polaczen', 'Mapa połączeń'),
('/forgot-password', 'Reset hasła'),
# ========== RAPORTY ==========
('/raporty/', 'Raporty'),
('/raporty/staz-czlonkostwa', 'Raport: Staż członkostwa'),
('/raporty/social-media', 'Raport: Social Media'),
('/raporty/struktura-branzowa', 'Raport: Struktura branżowa'),
# ========== ZOPK PUBLIC ==========
('/zopk', 'ZOPK: Strona główna'),
('/zopk/aktualnosci', 'ZOPK: Aktualności'),
# ========== CHAT ==========
('/chat', 'NordaGPT Chat'),
# ========== IT AUDIT ==========
('/it-audit/form', 'IT Audit: Formularz'),
# ========== PUBLIC API ==========
('/api/companies', 'API: Lista firm'),
('/api/model-info', 'API: Model info'),
('/api/gbp/audit/health', 'API: GBP health'),
# ========== ADMIN: CORE ==========
('/admin/security', 'Admin: Bezpieczeństwo'),
('/admin/analytics', 'Admin: Analityka'),
('/admin/status', 'Admin: Status systemu'),
('/admin/health', 'Admin: Health dashboard'),
('/admin/debug', 'Admin: Debug'),
('/admin/ai-usage', 'Admin: AI Usage'),
('/admin/chat-analytics', 'Admin: Chat analytics'),
('/admin/users', 'Admin: Użytkownicy'),
('/admin/recommendations', 'Admin: Rekomendacje'),
('/admin/fees', 'Admin: Składki'),
# ========== ADMIN: AUDITS ==========
('/admin/seo', 'Admin: SEO Audit'),
('/admin/gbp-audit', 'Admin: GBP Audit'),
('/admin/social-media', 'Admin: Social Media'),
('/admin/social-audit', 'Admin: Social Audit'),
('/admin/it-audit', 'Admin: IT Audit'),
('/admin/digital-maturity', 'Admin: Digital Maturity'),
('/admin/krs-audit', 'Admin: KRS Audit'),
# ========== ADMIN: COMMUNITY ==========
('/admin/forum', 'Admin: Forum'),
('/admin/kalendarz', 'Admin: Kalendarz'),
# ========== ADMIN: ZOPK ==========
('/admin/zopk', 'Admin: ZOPK Panel'),
('/admin/zopk/news', 'Admin: ZOPK News'),
('/admin/zopk/knowledge', 'Admin: ZOPK Knowledge'),
('/admin/zopk/knowledge/chunks', 'Admin: ZOPK Chunks'),
('/admin/zopk/knowledge/facts', 'Admin: ZOPK Facts'),
('/admin/zopk/knowledge/entities', 'Admin: ZOPK Entities'),
('/admin/zopk/knowledge/duplicates', 'Admin: ZOPK Duplikaty'),
('/admin/zopk/knowledge/fact-duplicates', 'Admin: ZOPK Fact Duplicates'),
('/admin/zopk/knowledge/graph', 'Admin: ZOPK Graf'),
('/admin/zopk/timeline', 'Admin: ZOPK Timeline'),
# ========== ZOPK API ==========
('/api/zopk/milestones', 'API: ZOPK Milestones'),
('/api/zopk/knowledge/dashboard-stats', 'API: ZOPK Dashboard stats'),
# ========== USER SETTINGS (v1.19.0) ==========
('/settings/privacy', 'Ustawienia: Prywatność'),
('/settings/blocks', 'Ustawienia: Blokady'),
('/settings/2fa', 'Ustawienia: 2FA'),
# ========== WIADOMOŚCI ==========
('/wiadomosci', 'Wiadomości: Odebrane'),
('/wiadomosci/wyslane', 'Wiadomości: Wysłane'),
('/wiadomosci/nowa', 'Wiadomości: Nowa'),
# ========== EDUKACJA ==========
('/edukacja', 'Edukacja: Strona główna'),
# ========== ADMIN: INSIGHTS ==========
('/admin/insights', 'Admin: Insights'),
]
# Dodaj losową firmę do sprawdzenia
db = SessionLocal()
try:
random_company = db.query(Company).first()
if random_company:
endpoints.append((f'/company/{random_company.slug}', f'Profil: {random_company.name[:25]}'))
finally:
db.close()
# Testuj każdy endpoint używając test client
with app.test_client() as client:
for path, name in endpoints:
try:
response = client.get(path, follow_redirects=False)
status = response.status_code
# 200 = OK, 302 = redirect (np. do logowania) = OK
# 429 = rate limited (endpoint działa, tylko ograniczony)
# 500 = błąd serwera, 404 = nie znaleziono
if status in (200, 302, 304, 429):
results.append({
'endpoint': path,
'name': name,
'status': status,
'ok': True
})
else:
results.append({
'endpoint': path,
'name': name,
'status': status,
'ok': False
})
all_ok = False
except Exception as e:
results.append({
'endpoint': path,
'name': name,
'status': 500,
'ok': False,
'error': str(e)[:100]
})
all_ok = False
# Podsumowanie
passed = sum(1 for r in results if r['ok'])
failed = len(results) - passed
return {
'status': 'ok' if all_ok else 'degraded',
'summary': {
'total': len(results),
'passed': passed,
'failed': failed
},
'endpoints': results,
'timestamp': datetime.now().isoformat()
}, 200 if all_ok else 503
# ============================================================
# PUBLIC ROUTES - MOVED TO blueprints/public/routes.py
# ============================================================
# The routes below have been migrated to the public blueprint.
# They are commented out but preserved for reference.
# See: blueprints/public/routes.py
# ============================================================
# RECOMMENDATIONS ADMIN ROUTES - MOVED TO: blueprints/admin/routes.py
# ============================================================
# ============================================================
# USER MANAGEMENT ADMIN ROUTES
# Moved to: blueprints/admin/routes.py
# NOTE: AI-parse routes remain below
# ============================================================
# admin_users, admin_user_add - MOVED TO: blueprints/admin/routes.py
# AI-ASSISTED USER CREATION - MOVED TO blueprints/admin/routes_users_api.py
# Routes: /admin/users-api/ai-parse, /admin/users-api/bulk-create
# ============================================================
# USER ANALYTICS API ROUTES - MOVED TO blueprints/api/routes_analytics.py
# ============================================================
# Routes: /api/analytics/track, /api/analytics/heartbeat, /api/analytics/scroll,
# /api/analytics/error, /api/analytics/performance, /api/analytics/conversion
# ============================================================
# RECOMMENDATIONS API ROUTES - MOVED TO blueprints/api/routes_recommendations.py
# ============================================================
# Routes: /api/recommendations/<company_id>, /api/recommendations/create,
# /api/recommendations/<rec_id>/edit, /api/recommendations/<rec_id>/delete
# ============================================================
# B2B CLASSIFIEDS ROUTES - MIGRATED TO blueprints/community/classifieds/
# ============================================================
# Routes: /tablica, /tablica/nowe, /tablica/<id>, /tablica/<id>/zakoncz
# ============================================================
# NEW MEMBERS ROUTE - MOVED TO blueprints/public/routes.py
# ============================================================
# AUTHENTICATION ROUTES - MOVED TO blueprints/auth/routes.py
# ============================================================
# The routes below have been migrated to the auth blueprint.
# They are commented out but preserved for reference.
# See: blueprints/auth/routes.py
# ============================================================
# TWO-FACTOR AUTHENTICATION - MOVED TO blueprints/auth/routes.py
# ============================================================
# MOJE KONTO - MOVED TO blueprints/auth/routes.py
# ============================================================
# USER DASHBOARD - MOVED TO blueprints/public/routes.py
# ============================================================
# API ROUTES - MOVED TO: blueprints/api/routes_company.py
# Routes: /api/companies, /api/connections, /api/check-email, /api/verify-nip,
# /api/verify-krs, /api/company/<id>/refresh-krs, /api/company/<id>/enrich-ai,
# /api/model-info, /api/admin/test-sanitization
# ============================================================
# ============================================================
# SEO/GBP/SOCIAL AUDIT API - MOVED TO: blueprints/api/routes_*_audit.py
# ============================================================
# ============================================================
# AUDIT DASHBOARDS - MOVED TO: blueprints/audit/routes.py
# ============================================================
# Validation and Company API routes moved to blueprints/api/routes_company.py
# ============================================================
# MODEL COMPARISON - Porównanie modeli AI
# ============================================================
# ============================================================
# SYSTEM STATUS DASHBOARD (Admin only)
# MOVED TO blueprints/admin/routes_status.py
# ============================================================
# ============================================================
# DEBUG PANEL (Admin only)
# ============================================================
# ============================================================
# SOCIAL MEDIA AUDIT ADMIN DASHBOARD
# ============================================================
# ============================================================
# IT AUDIT ADMIN DASHBOARD
# ============================================================
# ============================================================
# IT AUDIT FORM - MOVED TO blueprints/it_audit/
# ============================================================
# Routes: /it-audit/form, /it-audit/save, /api/it-audit/*
# ============================================================
# RAPORTY - MIGRATED TO blueprints/reports/
# ============================================================
# Routes: /raporty, /raporty/staz-czlonkostwa, /raporty/social-media, /raporty/struktura-branzowa
# RELEASE NOTES - MOVED TO blueprints/admin/routes.py (admin_notify_release)
# ============================================================
# ============================================================
# ZOPK PUBLIC ROUTES - MOVED TO blueprints/public/routes_zopk.py
# Routes: /zopk, /zopk/projekty/<slug>, /zopk/aktualnosci
# ============================================================
# ============================================================
# ZOPK ROUTES - MOVED TO BLUEPRINTS
# ============================================================
# All ZOPK routes have been migrated to:
# - blueprints/admin/routes_zopk_dashboard.py
# - blueprints/admin/routes_zopk_news.py
# - blueprints/admin/routes_zopk_knowledge.py
# - blueprints/admin/routes_zopk_timeline.py
# ============================================================
# Endpoint aliases for ZOPK are created in blueprints/__init__.py
# ============================================================
# KRS AUDIT (Krajowy Rejestr Sądowy)
# ============================================================
# ============================================================
# KRS API ROUTES - MOVED TO blueprints/admin/routes_krs_api.py
# ============================================================
# Routes: /admin/krs-api/audit, /admin/krs-api/audit/batch, /admin/krs-api/pdf/<company_id>
# ============================================================
# ERROR HANDLERS
# ============================================================
@app.errorhandler(404)
def not_found(error):
return render_template('errors/404.html'), 404
from flask_wtf.csrf import CSRFError
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
flash('Sesja wygasła lub formularz został nieprawidłowo przesłany. Spróbuj ponownie.', 'warning')
return redirect(request.referrer or url_for('index'))
def send_registration_notification(user_info):
"""Send email notification when a new user registers"""
try:
from email_service import send_email, is_configured
if not is_configured():
logger.warning("Email service not configured - skipping registration notification")
return
notify_email = os.getenv('ERROR_NOTIFY_EMAIL', 'maciej.pienczyn@inpi.pl')
if not notify_email:
return
reg_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
is_member = "✅ TAK" if user_info.get('is_norda_member') else "❌ NIE"
company_name = user_info.get('company_name', 'Brak przypisanej firmy')
subject = f"👤 NordaBiz: Nowa rejestracja - {user_info.get('name', 'Nieznany')}"
body_text = f"""👤 NOWA REJESTRACJA NA NORDABIZNES.PL
{'='*50}
🕐 Czas: {reg_time}
👤 Imię: {user_info.get('name', 'N/A')}
📧 Email: {user_info.get('email', 'N/A')}
🏢 NIP: {user_info.get('company_nip', 'N/A')}
🏛️ Firma: {company_name}
🎫 Członek NORDA: {is_member}
{'='*50}
🔗 Panel użytkowników: https://nordabiznes.pl/admin/users
"""
body_html = f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: 'Inter', Arial, sans-serif; background: #f8fafc; color: #1e293b; padding: 20px;">
<div style="max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #10b981, #059669); color: white; padding: 20px; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0; font-size: 20px;">👤 Nowa rejestracja na NordaBiznes.pl</h1>
</div>
<div style="background: white; padding: 25px; border-radius: 0 0 8px 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="color: #64748b; padding: 8px 0; border-bottom: 1px solid #e2e8f0;">🕐 Czas:</td><td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-weight: 500;">{reg_time}</td></tr>
<tr><td style="color: #64748b; padding: 8px 0; border-bottom: 1px solid #e2e8f0;">👤 Imię:</td><td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-weight: 600; color: #1e40af;">{user_info.get('name', 'N/A')}</td></tr>
<tr><td style="color: #64748b; padding: 8px 0; border-bottom: 1px solid #e2e8f0;">📧 Email:</td><td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0;"><a href="mailto:{user_info.get('email', '')}" style="color: #2563eb;">{user_info.get('email', 'N/A')}</a></td></tr>
<tr><td style="color: #64748b; padding: 8px 0; border-bottom: 1px solid #e2e8f0;">🏢 NIP:</td><td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-family: monospace;">{user_info.get('company_nip', 'N/A')}</td></tr>
<tr><td style="color: #64748b; padding: 8px 0; border-bottom: 1px solid #e2e8f0;">🏛️ Firma:</td><td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0;">{company_name}</td></tr>
<tr><td style="color: #64748b; padding: 8px 0;">🎫 Członek NORDA:</td><td style="padding: 8px 0; font-weight: 600;">{is_member}</td></tr>
</table>
<div style="margin-top: 25px; text-align: center;">
<a href="https://nordabiznes.pl/admin/users" style="display: inline-block; padding: 12px 24px; background: #2563eb; color: white; text-decoration: none; border-radius: 6px; font-weight: 500;">Otwórz panel użytkowników</a>
</div>
</div>
</div>
</body>
</html>"""
result = send_email(
to=[notify_email],
subject=subject,
body_text=body_text,
body_html=body_html,
email_type='registration_notification'
)
if result:
logger.info(f"Registration notification sent to {notify_email}")
else:
logger.error(f"Failed to send registration notification to {notify_email}")
except Exception as e:
logger.error(f"Failed to send registration notification: {e}")
def send_error_notification(error, request_info):
"""Send email notification about 500 errors via Microsoft Graph"""
try:
from email_service import send_email, is_configured
if not is_configured():
logger.warning("Email service not configured - skipping error notification")
return
error_email = os.getenv('ERROR_NOTIFY_EMAIL', 'maciej.pienczyn@inpi.pl')
if not error_email:
return
# Build error details
error_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
traceback_str = tb_module.format_exc()
subject = f"🚨 NordaBiz ERROR 500: {request_info.get('path', 'Unknown')}"
body_text = f"""⚠️ BŁĄD 500 NA NORDABIZNES.PL
{'='*50}
🕐 Czas: {error_time}
🌐 URL: {request_info.get('url', 'N/A')}
📍 Ścieżka: {request_info.get('path', 'N/A')}
📝 Metoda: {request_info.get('method', 'N/A')}
👤 Użytkownik: {request_info.get('user', 'Anonimowy')}
🖥️ IP: {request_info.get('ip', 'N/A')}
🌍 User-Agent: {request_info.get('user_agent', 'N/A')}
{'='*50}
📋 BŁĄD:
{str(error)}
{'='*50}
📜 TRACEBACK:
{traceback_str}
{'='*50}
🔧 Sprawdź logi: ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes --since '10 minutes ago'"
"""
body_html = f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: 'Courier New', monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px;">
<div style="max-width: 800px; margin: 0 auto;">
<div style="background: #dc2626; color: white; padding: 15px 20px; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0; font-size: 20px;">🚨 BŁĄD 500 NA NORDABIZNES.PL</h1>
</div>
<div style="background: #2d2d2d; padding: 20px; border-radius: 0 0 8px 8px;">
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="color: #9ca3af; padding: 5px 0;">🕐 Czas:</td><td style="color: #fbbf24;">{error_time}</td></tr>
<tr><td style="color: #9ca3af; padding: 5px 0;">🌐 URL:</td><td style="color: #60a5fa; word-break: break-all;">{request_info.get('url', 'N/A')}</td></tr>
<tr><td style="color: #9ca3af; padding: 5px 0;">📍 Ścieżka:</td><td style="color: #34d399;">{request_info.get('path', 'N/A')}</td></tr>
<tr><td style="color: #9ca3af; padding: 5px 0;">📝 Metoda:</td><td>{request_info.get('method', 'N/A')}</td></tr>
<tr><td style="color: #9ca3af; padding: 5px 0;">👤 Użytkownik:</td><td>{request_info.get('user', 'Anonimowy')}</td></tr>
<tr><td style="color: #9ca3af; padding: 5px 0;">🖥️ IP:</td><td>{request_info.get('ip', 'N/A')}</td></tr>
</table>
<div style="margin-top: 20px; padding: 15px; background: #1e1e1e; border-radius: 8px; border-left: 4px solid #dc2626;">
<div style="color: #f87171; font-weight: bold; margin-bottom: 10px;">📋 BŁĄD:</div>
<pre style="margin: 0; white-space: pre-wrap; color: #fca5a5;">{str(error)}</pre>
</div>
<div style="margin-top: 20px; padding: 15px; background: #1e1e1e; border-radius: 8px; border-left: 4px solid #f59e0b;">
<div style="color: #fbbf24; font-weight: bold; margin-bottom: 10px;">📜 TRACEBACK:</div>
<pre style="margin: 0; white-space: pre-wrap; font-size: 12px; color: #9ca3af; max-height: 400px; overflow: auto;">{traceback_str}</pre>
</div>
<div style="margin-top: 20px; padding: 15px; background: #1e3a5f; border-radius: 8px;">
<div style="color: #60a5fa;">🔧 <strong>Sprawdź logi:</strong></div>
<code style="display: block; margin-top: 10px; color: #34d399; word-break: break-all;">ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes --since '10 minutes ago'"</code>
</div>
</div>
</div>
</body>
</html>"""
result = send_email(
to=[error_email],
subject=subject,
body_text=body_text,
body_html=body_html,
email_type='error_notification'
)
if result:
logger.info(f"Error notification sent to {error_email}")
else:
logger.error(f"Failed to send error notification to {error_email}")
except Exception as e:
logger.error(f"Failed to send error notification: {e}")
@app.errorhandler(500)
def internal_error(error):
# Collect request info for notification
request_info = {
'url': request.url if request else 'N/A',
'path': request.path if request else 'N/A',
'method': request.method if request else 'N/A',
'ip': request.remote_addr if request else 'N/A',
'user_agent': request.headers.get('User-Agent', 'N/A') if request else 'N/A',
'user': current_user.email if current_user and current_user.is_authenticated else 'Anonimowy'
}
# Send notification in background (don't block response)
try:
send_error_notification(error, request_info)
except Exception as e:
logger.error(f"Error notification failed: {e}")
return render_template('errors/500.html'), 500
# ============================================================
# ADMIN - SECURITY DASHBOARD
# ============================================================
# ============================================================
# ANNOUNCEMENTS (Ogłoszenia dla członków)
# ============================================================
def generate_slug(title):
"""
Generate URL-friendly slug from title.
Uses unidecode for proper Polish character handling.
"""
import re
try:
from unidecode import unidecode
text = unidecode(title.lower())
except ImportError:
# Fallback without unidecode
text = title.lower()
replacements = {
'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n',
'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z'
}
for pl, en in replacements.items():
text = text.replace(pl, en)
# Remove special characters, replace spaces with hyphens
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[-\s]+', '-', text).strip('-')
return text[:200] # Limit slug length
# ============================================================
# PUBLIC ANNOUNCEMENTS - MOVED TO blueprints/public/routes_announcements.py
# ============================================================
# Routes: /ogloszenia, /ogloszenia/<slug>
# ============================================================
# EXTERNAL CONTACTS - PAGE ROUTES MIGRATED TO blueprints/community/contacts/
# ============================================================
# Routes: /kontakty, /kontakty/<id>, /kontakty/dodaj, /kontakty/<id>/edytuj, /kontakty/<id>/usun
# API routes remain below for backwards compatibility
# ============================================================
# CONTACTS API ROUTES - MOVED TO blueprints/api/routes_contacts.py
# ============================================================
# Routes: /api/contacts/ai-parse, /api/contacts/bulk-create
# Includes AI prompts for contact parsing
# ============================================================
# HONEYPOT ENDPOINTS (trap for malicious bots)
# ============================================================
@app.route('/wp-admin')
@app.route('/wp-admin/<path:path>')
@app.route('/wp-login.php')
@app.route('/administrator')
@app.route('/phpmyadmin')
@app.route('/phpmyadmin/<path:path>')
@app.route('/.env')
@app.route('/.git/config')
@app.route('/xmlrpc.php')
@app.route('/config.php')
@app.route('/admin.php')
def honeypot_trap(path=None):
"""
Honeypot endpoints - log and return 404.
These URLs are commonly probed by malicious bots looking for WordPress,
phpMyAdmin, or exposed configuration files.
"""
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if client_ip and ',' in client_ip:
client_ip = client_ip.split(',')[0].strip()
security_logger.warning(f"HONEYPOT ip={client_ip} path={request.path} ua={request.user_agent.string[:100]}")
# Return 404 to not reveal this is a trap
return render_template('errors/404.html'), 404
# ============================================================
# MAIN
# ============================================================
if __name__ == '__main__':
# Port 5001 jako domyślny - macOS AirPlay zajmuje 5000
port = int(os.getenv('PORT', 5001))
debug = os.getenv('FLASK_ENV') == 'development'
logger.info(f"Starting Norda Biznes Partner on port {port}")
app.run(host='0.0.0.0', port=port, debug=debug)