nordabiz/blueprints/api/routes_gbp_audit.py
Maciej Pienczyn 7f77d7ebcd
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(security): Restrict audit access to single designated user
Audits (SEO, IT, GBP, Social Media) are now visible only to the
designated audit owner (maciej.pienczyn@inpi.pl). All other users,
including admins, see 404 for audit routes and no audit links in
navigation. KRS Audit and Digital Maturity remain unchanged.

Adds /admin/access-overview panel showing the access matrix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 12:31:10 +01:00

396 lines
14 KiB
Python

"""
GBP Audit API Routes - API blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains API routes for Google Business Profile audit functionality.
"""
import logging
from datetime import datetime
from flask import abort, jsonify, request, current_app
from flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company
from . import bp
logger = logging.getLogger(__name__)
# Check if GBP audit service is available
try:
from gbp_audit_service import (
AuditResult as GBPAuditResult,
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:
logger.warning(f"GBP audit service not available: {e}")
GBP_AUDIT_AVAILABLE = False
GBP_AUDIT_VERSION = None
def get_limiter():
"""Get rate limiter from current app."""
return current_app.extensions.get('limiter')
# ============================================================
# GBP AUDIT API ROUTES
# ============================================================
@bp.route('/gbp/audit/health')
def api_gbp_audit_health():
"""
API: Health check for GBP audit service.
Returns service status and version information.
Used by monitoring systems to verify service availability.
"""
if not is_audit_owner():
return jsonify({'success': False, 'error': 'Nie znaleziono'}), 404
if GBP_AUDIT_AVAILABLE:
return jsonify({
'status': 'ok',
'service': 'gbp_audit',
'version': GBP_AUDIT_VERSION,
'available': True
}), 200
else:
return jsonify({
'status': 'unavailable',
'service': 'gbp_audit',
'available': False,
'error': 'GBP audit service not loaded'
}), 503
@bp.route('/gbp/audit', methods=['GET'])
def api_gbp_audit_get():
"""
API: Get GBP audit results for a company.
Query parameters:
- company_id: Company ID (integer) OR
- slug: Company slug (string)
Returns:
- Latest audit results with completeness score and recommendations
- 404 if company not found
- 404 if no audit exists for the company
Example: GET /api/gbp/audit?company_id=26
Example: GET /api/gbp/audit?slug=pixlab-sp-z-o-o
"""
if not is_audit_owner():
return jsonify({'success': False, 'error': 'Nie znaleziono'}), 404
if not GBP_AUDIT_AVAILABLE:
return jsonify({
'success': False,
'error': 'Usługa audytu GBP jest niedostępna.'
}), 503
company_id = request.args.get('company_id', type=int)
slug = request.args.get('slug')
if not company_id and not slug:
return jsonify({
'success': False,
'error': 'Podaj company_id lub slug firmy.'
}), 400
db = SessionLocal()
try:
# Find company
if company_id:
company = db.query(Company).filter_by(id=company_id, status='active').first()
else:
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
return jsonify({
'success': False,
'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404
# Get latest audit
audit = gbp_get_company_audit(db, company.id)
if not audit:
return jsonify({
'success': False,
'error': f'Brak wyników audytu GBP dla firmy "{company.name}". Uruchom audyt używając POST /api/gbp/audit.',
'company_id': company.id,
'company_name': company.name
}), 404
# Build response
return jsonify({
'success': True,
'company_id': company.id,
'company_name': company.name,
'company_slug': company.slug,
'audit': {
'id': audit.id,
'audit_date': audit.audit_date.isoformat() if audit.audit_date else None,
'completeness_score': audit.completeness_score,
'score_category': audit.score_category,
'fields_status': audit.fields_status,
'recommendations': audit.recommendations,
'has_name': audit.has_name,
'has_address': audit.has_address,
'has_phone': audit.has_phone,
'has_website': audit.has_website,
'has_hours': audit.has_hours,
'has_categories': audit.has_categories,
'has_photos': audit.has_photos,
'has_description': audit.has_description,
'has_services': audit.has_services,
'has_reviews': audit.has_reviews,
'photo_count': audit.photo_count,
'review_count': audit.review_count,
'average_rating': float(audit.average_rating) if audit.average_rating else None,
'google_place_id': audit.google_place_id,
'audit_source': audit.audit_source,
'audit_version': audit.audit_version
}
}), 200
except Exception as e:
logger.error(f"Error fetching GBP audit: {e}")
return jsonify({
'success': False,
'error': f'Błąd podczas pobierania audytu: {str(e)}'
}), 500
finally:
db.close()
@bp.route('/gbp/audit/<slug>')
def api_gbp_audit_by_slug(slug):
"""
API: Get GBP audit results for a company by slug.
Convenience endpoint that uses slug from URL path.
Example: GET /api/gbp/audit/pixlab-sp-z-o-o
"""
if not is_audit_owner():
return jsonify({'success': False, 'error': 'Nie znaleziono'}), 404
if not GBP_AUDIT_AVAILABLE:
return jsonify({
'success': False,
'error': 'Usługa audytu GBP jest niedostępna.'
}), 503
db = SessionLocal()
try:
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
return jsonify({
'success': False,
'error': f'Firma o slug "{slug}" nie znaleziona.'
}), 404
audit = gbp_get_company_audit(db, company.id)
if not audit:
return jsonify({
'success': False,
'error': f'Brak wyników audytu GBP dla firmy "{company.name}".',
'company_id': company.id,
'company_name': company.name
}), 404
return jsonify({
'success': True,
'company_id': company.id,
'company_name': company.name,
'company_slug': company.slug,
'audit': {
'id': audit.id,
'audit_date': audit.audit_date.isoformat() if audit.audit_date else None,
'completeness_score': audit.completeness_score,
'score_category': audit.score_category,
'fields_status': audit.fields_status,
'recommendations': audit.recommendations,
'photo_count': audit.photo_count,
'review_count': audit.review_count,
'average_rating': float(audit.average_rating) if audit.average_rating else None
}
}), 200
finally:
db.close()
@bp.route('/gbp/audit', methods=['POST'])
@login_required
def api_gbp_audit_trigger():
"""
API: Run GBP audit for a company.
This endpoint runs a completeness audit for Google Business Profile data,
checking fields like name, address, phone, website, hours, categories,
photos, description, services, and reviews.
Request JSON body:
- company_id: Company ID (integer) OR
- slug: Company slug (string)
- save: Whether to save results to database (default: true)
Returns:
- Success: Audit results with completeness score and recommendations
- Error: Error message with status code
Access:
- Members can audit their own company
- Admins can audit any company
Rate limited to 20 requests per hour per user.
"""
if not is_audit_owner():
abort(404)
if not GBP_AUDIT_AVAILABLE:
return jsonify({
'success': False,
'error': 'Usługa audytu GBP jest niedostępna. Sprawdź konfigurację serwera.'
}), 503
# Parse request data
data = request.get_json()
if not data:
return jsonify({
'success': False,
'error': 'Brak danych w żądaniu. Podaj company_id lub slug.'
}), 400
company_id = data.get('company_id')
slug = data.get('slug')
save_result = data.get('save', True)
if not company_id and not slug:
return jsonify({
'success': False,
'error': 'Podaj company_id lub slug firmy do audytu.'
}), 400
db = SessionLocal()
try:
# Find company by ID or slug
if company_id:
company = db.query(Company).filter_by(id=company_id, status='active').first()
else:
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
return jsonify({
'success': False,
'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404
# Check access: users with company edit rights can audit
if not current_user.can_edit_company(company.id):
return jsonify({
'success': False,
'error': 'Brak uprawnień. Możesz audytować tylko własną firmę.'
}), 403
logger.info(f"GBP audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})")
# Option to fetch fresh Google data before audit
fetch_google = data.get('fetch_google', True)
force_refresh = data.get('force_refresh', False)
try:
# Step 1: Fetch fresh Google Business data (if enabled)
fetch_result = None
if fetch_google:
logger.info(f"Fetching Google Business data for company {company.id}...")
fetch_result = gbp_fetch_google_data(db, company.id, force_refresh=force_refresh)
if not fetch_result.get('success') and not fetch_result.get('data', {}).get('cached'):
# Log warning but continue with audit
logger.warning(f"Google fetch warning for company {company.id}: {fetch_result.get('error')}")
# Step 2: Run the audit
result = gbp_audit_company(db, company.id, save=save_result)
# Build field status for response
fields_response = {}
for field_name, field_status in result.fields.items():
fields_response[field_name] = {
'status': field_status.status,
'value': str(field_status.value) if field_status.value is not None else None,
'score': field_status.score,
'max_score': field_status.max_score,
'recommendation': field_status.recommendation
}
# Determine score category
score = result.completeness_score
if score >= 90:
score_category = 'excellent'
elif score >= 70:
score_category = 'good'
elif score >= 50:
score_category = 'needs_work'
else:
score_category = 'poor'
response_data = {
'success': True,
'message': f'Audyt GBP dla firmy "{company.name}" został zakończony pomyślnie.',
'company_id': company.id,
'company_name': company.name,
'company_slug': company.slug,
'audit_version': GBP_AUDIT_VERSION,
'triggered_by': current_user.email,
'triggered_at': datetime.now().isoformat(),
'saved': save_result,
'audit': {
'completeness_score': result.completeness_score,
'score_category': score_category,
'fields_status': fields_response,
'recommendations': result.recommendations,
'photo_count': result.photo_count,
'logo_present': result.logo_present,
'cover_photo_present': result.cover_photo_present,
'review_count': result.review_count,
'average_rating': float(result.average_rating) if result.average_rating else None,
'google_place_id': result.google_place_id
}
}
# Include Google fetch results if performed
if fetch_result:
response_data['google_fetch'] = {
'success': fetch_result.get('success', False),
'steps': fetch_result.get('steps', []),
'data': fetch_result.get('data', {}),
'error': fetch_result.get('error')
}
return jsonify(response_data), 200
except ValueError as e:
return jsonify({
'success': False,
'error': str(e),
'company_id': company.id if company else None
}), 400
except Exception as e:
logger.error(f"GBP audit error for company {company.id}: {e}")
return jsonify({
'success': False,
'error': f'Błąd podczas wykonywania audytu: {str(e)}',
'company_id': company.id,
'company_name': company.name
}), 500
finally:
db.close()