diff --git a/app.py b/app.py index 967e836..cc2e297 100644 --- a/app.py +++ b/app.py @@ -346,6 +346,12 @@ def inject_globals(): } +@app.context_processor +def inject_audit_access(): + from utils.decorators import is_audit_owner + return dict(is_audit_owner=is_audit_owner()) + + @app.context_processor def inject_notifications(): """Inject unread notifications count into all templates""" diff --git a/blueprints/admin/routes_audits.py b/blueprints/admin/routes_audits.py index c632a4e..aa3be80 100644 --- a/blueprints/admin/routes_audits.py +++ b/blueprints/admin/routes_audits.py @@ -8,7 +8,7 @@ SEO and GBP audit dashboards for admin panel. import logging from datetime import datetime -from flask import render_template, request, redirect, url_for, flash +from flask import abort, render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from . import bp @@ -17,7 +17,7 @@ from database import ( CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson, ITAudit, ITCollaborationMatch, SystemRole ) -from utils.decorators import role_required +from utils.decorators import role_required, is_audit_owner logger = logging.getLogger(__name__) @@ -44,6 +44,8 @@ def admin_seo(): Query Parameters: - company: Slug of company to highlight/filter (optional) """ + if not is_audit_owner(): + abort(404) # Get optional company filter from URL filter_company_slug = request.args.get('company', '') @@ -154,6 +156,8 @@ def admin_gbp_audit(): - Review metrics (avg rating, review counts) - Photo statistics """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: from sqlalchemy import func @@ -499,6 +503,8 @@ def admin_it_audit(): Access: Office Manager and above """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: from sqlalchemy import func @@ -723,3 +729,32 @@ def admin_it_audit(): finally: db.close() + + +# ============================================================ +# ACCESS OVERVIEW DASHBOARD +# ============================================================ + +@bp.route('/access-overview') +@login_required +@role_required(SystemRole.OFFICE_MANAGER) +def admin_access_overview(): + """Panel kontroli dostepu — kto widzi co.""" + if not is_audit_owner(): + abort(404) + + from database import User + db = SessionLocal() + try: + users = db.query(User).filter( + User.is_active == True, + User.role.in_(['OFFICE_MANAGER', 'ADMIN']) + ).order_by(User.name).all() + + from utils.decorators import AUDIT_OWNER_EMAIL + return render_template('admin/access_overview.html', + users=users, + audit_owner_email=AUDIT_OWNER_EMAIL + ) + finally: + db.close() diff --git a/blueprints/admin/routes_social.py b/blueprints/admin/routes_social.py index 6989cb1..9374c31 100644 --- a/blueprints/admin/routes_social.py +++ b/blueprints/admin/routes_social.py @@ -16,7 +16,7 @@ from . import bp from database import ( SessionLocal, Company, Category, CompanySocialMedia, SystemRole ) -from utils.decorators import role_required +from utils.decorators import role_required, is_audit_owner logger = logging.getLogger(__name__) @@ -132,6 +132,9 @@ def admin_social_audit(): - Sortable table with platform icons per company - Followers aggregate statistics """ + if not is_audit_owner(): + from flask import abort + abort(404) db = SessionLocal() try: # Platform definitions diff --git a/blueprints/api/routes_audit_actions.py b/blueprints/api/routes_audit_actions.py index e94ae3a..5205ae2 100644 --- a/blueprints/api/routes_audit_actions.py +++ b/blueprints/api/routes_audit_actions.py @@ -7,8 +7,9 @@ Uses audit_ai_service.py for Gemini integration. import logging -from flask import jsonify, request +from flask import abort, jsonify, request from flask_login import current_user, login_required +from utils.decorators import is_audit_owner from database import SessionLocal, Company from . import bp @@ -30,6 +31,8 @@ def api_audit_analyze(): Returns: JSON with summary and actions list """ + if not is_audit_owner(): + abort(404) import audit_ai_service data = request.get_json() @@ -86,6 +89,8 @@ def api_audit_generate_content(): Returns: JSON with generated content """ + if not is_audit_owner(): + abort(404) import audit_ai_service data = request.get_json() @@ -133,6 +138,8 @@ def api_audit_actions_by_slug(slug): Returns: JSON with list of actions """ + if not is_audit_owner(): + abort(404) import audit_ai_service audit_type = request.args.get('audit_type') @@ -173,6 +180,8 @@ def api_audit_action_update_status(action_id): Returns: JSON with updated status """ + if not is_audit_owner(): + abort(404) import audit_ai_service from database import AuditAction diff --git a/blueprints/api/routes_gbp_audit.py b/blueprints/api/routes_gbp_audit.py index 846105c..67a48de 100644 --- a/blueprints/api/routes_gbp_audit.py +++ b/blueprints/api/routes_gbp_audit.py @@ -8,8 +8,9 @@ Contains API routes for Google Business Profile audit functionality. import logging from datetime import datetime -from flask import jsonify, request, current_app +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 @@ -50,6 +51,8 @@ def api_gbp_audit_health(): 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', @@ -83,6 +86,8 @@ def api_gbp_audit_get(): 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, @@ -173,6 +178,8 @@ def api_gbp_audit_by_slug(slug): 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, @@ -246,6 +253,8 @@ def api_gbp_audit_trigger(): 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, diff --git a/blueprints/api/routes_seo_audit.py b/blueprints/api/routes_seo_audit.py index b21b7c7..3ce8358 100644 --- a/blueprints/api/routes_seo_audit.py +++ b/blueprints/api/routes_seo_audit.py @@ -10,8 +10,9 @@ import sys from datetime import datetime from pathlib import Path -from flask import jsonify, request, current_app +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, CompanyWebsiteAnalysis from . import bp @@ -267,6 +268,8 @@ def api_seo_audit(): - technical checks (ssl, sitemap, robots.txt, mobile-friendly) - issues list with severity levels """ + if not is_audit_owner(): + return jsonify({'success': False, 'error': 'Nie znaleziono'}), 404 company_id = request.args.get('company_id', type=int) slug = request.args.get('slug', type=str) @@ -305,6 +308,8 @@ def api_seo_audit_by_slug(slug): Example: GET /api/seo/audit/pixlab-sp-z-o-o """ + if not is_audit_owner(): + return jsonify({'success': False, 'error': 'Nie znaleziono'}), 404 db = SessionLocal() try: # Find company by slug @@ -342,6 +347,8 @@ def api_seo_audit_trigger(): - Success: Full SEO audit results saved to database - Error: Error message with status code """ + if not is_audit_owner(): + abort(404) # Check admin panel access if not current_user.can_access_admin_panel(): return jsonify({ diff --git a/blueprints/api/routes_social_audit.py b/blueprints/api/routes_social_audit.py index 1ada97e..c957f3c 100644 --- a/blueprints/api/routes_social_audit.py +++ b/blueprints/api/routes_social_audit.py @@ -9,8 +9,9 @@ import logging import sys from pathlib import Path -from flask import jsonify, request +from flask import abort, jsonify, request from flask_login import current_user, login_required +from utils.decorators import is_audit_owner from database import SessionLocal, Company from . import bp @@ -44,6 +45,8 @@ def api_social_audit_trigger(): Rate limited to 10 requests per hour per user. """ + if not is_audit_owner(): + abort(404) # Import the SocialMediaAuditor from scripts try: scripts_dir = Path(__file__).parent.parent.parent / 'scripts' diff --git a/blueprints/audit/routes.py b/blueprints/audit/routes.py index cddbe67..d43b462 100644 --- a/blueprints/audit/routes.py +++ b/blueprints/audit/routes.py @@ -7,8 +7,9 @@ Contains user-facing audit dashboard pages for SEO, GBP, Social Media, and IT. import logging -from flask import flash, redirect, render_template, url_for +from flask import abort, flash, redirect, render_template, url_for from flask_login import current_user, login_required +from utils.decorators import is_audit_owner from database import ( SessionLocal, Company, CompanyWebsiteAnalysis, @@ -57,6 +58,8 @@ def seo_audit_dashboard(slug): Returns: Rendered seo_audit.html template with company and audit data """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: # Find company by slug @@ -236,6 +239,8 @@ def social_audit_dashboard(slug): Returns: Rendered social_audit.html template with company and social data """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: # Find company by slug @@ -338,6 +343,8 @@ def gbp_audit_dashboard(slug): Returns: Rendered gbp_audit.html template with company and audit data """ + if not is_audit_owner(): + abort(404) if not GBP_AUDIT_AVAILABLE: flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error') return redirect(url_for('dashboard')) @@ -443,6 +450,8 @@ def it_audit_dashboard(slug): Returns: Rendered it_audit.html template with company and audit data """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: # Find company by slug diff --git a/blueprints/it_audit/routes.py b/blueprints/it_audit/routes.py index 89c3f6d..caa1f28 100644 --- a/blueprints/it_audit/routes.py +++ b/blueprints/it_audit/routes.py @@ -10,8 +10,9 @@ import json import logging from io import StringIO -from flask import flash, jsonify, redirect, render_template, request, Response, url_for +from flask import abort, flash, jsonify, redirect, render_template, request, Response, url_for from flask_login import current_user, login_required +from utils.decorators import is_audit_owner from database import SessionLocal, Company from . import bp @@ -60,6 +61,8 @@ def it_audit_form(): Returns: Rendered it_audit_form.html template with company and audit data """ + if not is_audit_owner(): + abort(404) db = SessionLocal() try: from database import ITAudit @@ -136,6 +139,8 @@ def it_audit_save(): Rate limited to 30 requests per hour per user. """ + if not is_audit_owner(): + abort(404) # Apply rate limiting manually since decorator doesn't work with blueprint limiter = get_limiter() if limiter: diff --git a/blueprints/it_audit/routes_api.py b/blueprints/it_audit/routes_api.py index 60b43e6..8572b43 100644 --- a/blueprints/it_audit/routes_api.py +++ b/blueprints/it_audit/routes_api.py @@ -9,8 +9,9 @@ import csv import logging from io import StringIO -from flask import jsonify, request, Response +from flask import abort, jsonify, request, Response from flask_login import current_user, login_required +from utils.decorators import is_audit_owner from database import SessionLocal, Company from . import bp @@ -43,6 +44,8 @@ def api_it_audit_matches(company_id): - partner company info (id, name, slug) - match_reason and shared_attributes """ + if not is_audit_owner(): + abort(404) # Only users with admin panel access can view collaboration matches if not current_user.can_access_admin_panel(): return jsonify({ @@ -136,6 +139,8 @@ def api_it_audit_history(company_id): - audit_id, audit_date, overall_score, scores, maturity_level - is_current flag (True for the most recent audit) """ + if not is_audit_owner(): + abort(404) from it_audit_service import get_company_audit_history # Access control: users with company edit rights can view history @@ -210,6 +215,8 @@ def api_it_audit_export(): Returns: CSV file with IT audit data """ + if not is_audit_owner(): + abort(404) if not current_user.can_access_admin_panel(): return jsonify({ 'success': False, diff --git a/templates/admin/access_overview.html b/templates/admin/access_overview.html new file mode 100644 index 0000000..00f9cb9 --- /dev/null +++ b/templates/admin/access_overview.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} + +{% block title %}Kontrola dostepu - Admin{% endblock %} + +{% block content %} +
+ + + +
+
+

Zasada ograniczenia dostepu

+

+ Audyty SEO, IT, GBP i Social Media sa widoczne wylacznie dla {{ audit_owner_email }}. + Pozostali administratorzy nie widza tych funkcji w menu ani na stronach firm. + Audyt KRS i Digital Maturity pozostaja dostepne dla wszystkich z rola OFFICE_MANAGER+. +

+
+
+ + +
+
+ + + + + + + + + + + + + + + + + {% for user in users %} + + + + + {% set is_owner = user.email == audit_owner_email %} + + + + + + + + + {% endfor %} + +
UzytkownikEmailRolaAudyt SEOAudyt ITAudyt GBPAudyt SocialAudyt KRSDigital MaturityInne admin
{{ user.name or 'Brak nazwy' }}{{ user.email }} + + {{ user.role }} + + + {% if is_owner %} + + {% else %} + + {% endif %} + + {% if is_owner %} + + {% else %} + + {% endif %} + + {% if is_owner %} + + {% else %} + + {% endif %} + + {% if is_owner %} + + {% else %} + + {% endif %} + + + + + + +
+
+
+ + +
+
+

Odwracalnosc

+

+ Aby przywrocic dostep do audytow dla wszystkich administratorow, + nalezy zmienic funkcje is_audit_owner() w pliku + utils/decorators.py na sprawdzanie roli OFFICE_MANAGER. +

+
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 4ee0615..deaf2d2 100755 --- a/templates/base.html +++ b/templates/base.html @@ -1518,6 +1518,7 @@
+ {% if is_audit_owner %} @@ -1543,6 +1544,13 @@ Audyt Social + + + + + Kontrola dostepu + + {% endif %} diff --git a/templates/company_detail.html b/templates/company_detail.html index 5de9289..2606056 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -818,7 +818,7 @@
{# Audit links - separate row, visible only to authorized users #} -{% if current_user.is_authenticated and current_user.can_edit_company(company.id) %} +{% if is_audit_owner %}
@@ -2747,7 +2747,7 @@ {% endif %} -{% if website_analysis and website_analysis.seo_audited_at %} +{% if website_analysis and website_analysis.seo_audited_at and is_audit_owner %}

Analiza SEO @@ -3078,7 +3078,7 @@ {% endif %} -{% if gbp_audit and gbp_audit.completeness_score is not none %} +{% if gbp_audit and gbp_audit.completeness_score is not none and is_audit_owner %}

Audyt Google Business Profile @@ -3291,7 +3291,7 @@ {% endif %} -{% if social_media and social_media|length > 0 %} +{% if social_media and social_media|length > 0 and is_audit_owner %}

Audyt Social Media @@ -3362,7 +3362,7 @@ {% endif %} -{% if it_audit and it_audit.overall_score is not none %} +{% if it_audit and it_audit.overall_score is not none and is_audit_owner %}

Audyt IT diff --git a/utils/decorators.py b/utils/decorators.py index 98d2add..78d3498 100644 --- a/utils/decorators.py +++ b/utils/decorators.py @@ -18,6 +18,15 @@ from functools import wraps from flask import abort, flash, redirect, url_for, request from flask_login import current_user +# Audit access restriction — only this user sees SEO/IT/GBP/Social audits +AUDIT_OWNER_EMAIL = 'maciej.pienczyn@inpi.pl' + +def is_audit_owner(): + """True only for the designated audit owner.""" + if not current_user.is_authenticated: + return False + return current_user.email == AUDIT_OWNER_EMAIL + # Import role enums (lazy import to avoid circular dependencies) def _get_system_role(): from database import SystemRole