feat(security): Restrict audit access to single designated user
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

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>
This commit is contained in:
Maciej Pienczyn 2026-02-09 12:31:10 +01:00
parent e8b7f2214f
commit 7f77d7ebcd
14 changed files with 236 additions and 15 deletions

6
app.py
View File

@ -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 @app.context_processor
def inject_notifications(): def inject_notifications():
"""Inject unread notifications count into all templates""" """Inject unread notifications count into all templates"""

View File

@ -8,7 +8,7 @@ SEO and GBP audit dashboards for admin panel.
import logging import logging
from datetime import datetime 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 flask_login import login_required, current_user
from . import bp from . import bp
@ -17,7 +17,7 @@ from database import (
CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson, CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson,
ITAudit, ITCollaborationMatch, SystemRole ITAudit, ITCollaborationMatch, SystemRole
) )
from utils.decorators import role_required from utils.decorators import role_required, is_audit_owner
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,6 +44,8 @@ def admin_seo():
Query Parameters: Query Parameters:
- company: Slug of company to highlight/filter (optional) - company: Slug of company to highlight/filter (optional)
""" """
if not is_audit_owner():
abort(404)
# Get optional company filter from URL # Get optional company filter from URL
filter_company_slug = request.args.get('company', '') filter_company_slug = request.args.get('company', '')
@ -154,6 +156,8 @@ def admin_gbp_audit():
- Review metrics (avg rating, review counts) - Review metrics (avg rating, review counts)
- Photo statistics - Photo statistics
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func from sqlalchemy import func
@ -499,6 +503,8 @@ def admin_it_audit():
Access: Office Manager and above Access: Office Manager and above
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func from sqlalchemy import func
@ -723,3 +729,32 @@ def admin_it_audit():
finally: finally:
db.close() 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()

View File

@ -16,7 +16,7 @@ from . import bp
from database import ( from database import (
SessionLocal, Company, Category, CompanySocialMedia, SystemRole SessionLocal, Company, Category, CompanySocialMedia, SystemRole
) )
from utils.decorators import role_required from utils.decorators import role_required, is_audit_owner
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -132,6 +132,9 @@ def admin_social_audit():
- Sortable table with platform icons per company - Sortable table with platform icons per company
- Followers aggregate statistics - Followers aggregate statistics
""" """
if not is_audit_owner():
from flask import abort
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
# Platform definitions # Platform definitions

View File

@ -7,8 +7,9 @@ Uses audit_ai_service.py for Gemini integration.
import logging import logging
from flask import jsonify, request from flask import abort, jsonify, request
from flask_login import current_user, login_required from flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company from database import SessionLocal, Company
from . import bp from . import bp
@ -30,6 +31,8 @@ def api_audit_analyze():
Returns: Returns:
JSON with summary and actions list JSON with summary and actions list
""" """
if not is_audit_owner():
abort(404)
import audit_ai_service import audit_ai_service
data = request.get_json() data = request.get_json()
@ -86,6 +89,8 @@ def api_audit_generate_content():
Returns: Returns:
JSON with generated content JSON with generated content
""" """
if not is_audit_owner():
abort(404)
import audit_ai_service import audit_ai_service
data = request.get_json() data = request.get_json()
@ -133,6 +138,8 @@ def api_audit_actions_by_slug(slug):
Returns: Returns:
JSON with list of actions JSON with list of actions
""" """
if not is_audit_owner():
abort(404)
import audit_ai_service import audit_ai_service
audit_type = request.args.get('audit_type') audit_type = request.args.get('audit_type')
@ -173,6 +180,8 @@ def api_audit_action_update_status(action_id):
Returns: Returns:
JSON with updated status JSON with updated status
""" """
if not is_audit_owner():
abort(404)
import audit_ai_service import audit_ai_service
from database import AuditAction from database import AuditAction

View File

@ -8,8 +8,9 @@ Contains API routes for Google Business Profile audit functionality.
import logging import logging
from datetime import datetime 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 flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company from database import SessionLocal, Company
from . import bp from . import bp
@ -50,6 +51,8 @@ def api_gbp_audit_health():
Returns service status and version information. Returns service status and version information.
Used by monitoring systems to verify service availability. 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: if GBP_AUDIT_AVAILABLE:
return jsonify({ return jsonify({
'status': 'ok', 'status': 'ok',
@ -83,6 +86,8 @@ def api_gbp_audit_get():
Example: GET /api/gbp/audit?company_id=26 Example: GET /api/gbp/audit?company_id=26
Example: GET /api/gbp/audit?slug=pixlab-sp-z-o-o 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: if not GBP_AUDIT_AVAILABLE:
return jsonify({ return jsonify({
'success': False, 'success': False,
@ -173,6 +178,8 @@ def api_gbp_audit_by_slug(slug):
Example: GET /api/gbp/audit/pixlab-sp-z-o-o 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: if not GBP_AUDIT_AVAILABLE:
return jsonify({ return jsonify({
'success': False, 'success': False,
@ -246,6 +253,8 @@ def api_gbp_audit_trigger():
Rate limited to 20 requests per hour per user. Rate limited to 20 requests per hour per user.
""" """
if not is_audit_owner():
abort(404)
if not GBP_AUDIT_AVAILABLE: if not GBP_AUDIT_AVAILABLE:
return jsonify({ return jsonify({
'success': False, 'success': False,

View File

@ -10,8 +10,9 @@ import sys
from datetime import datetime from datetime import datetime
from pathlib import Path 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 flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company, CompanyWebsiteAnalysis from database import SessionLocal, Company, CompanyWebsiteAnalysis
from . import bp from . import bp
@ -267,6 +268,8 @@ def api_seo_audit():
- technical checks (ssl, sitemap, robots.txt, mobile-friendly) - technical checks (ssl, sitemap, robots.txt, mobile-friendly)
- issues list with severity levels - 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) company_id = request.args.get('company_id', type=int)
slug = request.args.get('slug', type=str) 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 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() db = SessionLocal()
try: try:
# Find company by slug # Find company by slug
@ -342,6 +347,8 @@ def api_seo_audit_trigger():
- Success: Full SEO audit results saved to database - Success: Full SEO audit results saved to database
- Error: Error message with status code - Error: Error message with status code
""" """
if not is_audit_owner():
abort(404)
# Check admin panel access # Check admin panel access
if not current_user.can_access_admin_panel(): if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({

View File

@ -9,8 +9,9 @@ import logging
import sys import sys
from pathlib import Path from pathlib import Path
from flask import jsonify, request from flask import abort, jsonify, request
from flask_login import current_user, login_required from flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company from database import SessionLocal, Company
from . import bp from . import bp
@ -44,6 +45,8 @@ def api_social_audit_trigger():
Rate limited to 10 requests per hour per user. Rate limited to 10 requests per hour per user.
""" """
if not is_audit_owner():
abort(404)
# Import the SocialMediaAuditor from scripts # Import the SocialMediaAuditor from scripts
try: try:
scripts_dir = Path(__file__).parent.parent.parent / 'scripts' scripts_dir = Path(__file__).parent.parent.parent / 'scripts'

View File

@ -7,8 +7,9 @@ Contains user-facing audit dashboard pages for SEO, GBP, Social Media, and IT.
import logging 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 flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import ( from database import (
SessionLocal, Company, CompanyWebsiteAnalysis, SessionLocal, Company, CompanyWebsiteAnalysis,
@ -57,6 +58,8 @@ def seo_audit_dashboard(slug):
Returns: Returns:
Rendered seo_audit.html template with company and audit data Rendered seo_audit.html template with company and audit data
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
# Find company by slug # Find company by slug
@ -236,6 +239,8 @@ def social_audit_dashboard(slug):
Returns: Returns:
Rendered social_audit.html template with company and social data Rendered social_audit.html template with company and social data
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
# Find company by slug # Find company by slug
@ -338,6 +343,8 @@ def gbp_audit_dashboard(slug):
Returns: Returns:
Rendered gbp_audit.html template with company and audit data Rendered gbp_audit.html template with company and audit data
""" """
if not is_audit_owner():
abort(404)
if not GBP_AUDIT_AVAILABLE: if not GBP_AUDIT_AVAILABLE:
flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error') flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
@ -443,6 +450,8 @@ def it_audit_dashboard(slug):
Returns: Returns:
Rendered it_audit.html template with company and audit data Rendered it_audit.html template with company and audit data
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
# Find company by slug # Find company by slug

View File

@ -10,8 +10,9 @@ import json
import logging import logging
from io import StringIO 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 flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company from database import SessionLocal, Company
from . import bp from . import bp
@ -60,6 +61,8 @@ def it_audit_form():
Returns: Returns:
Rendered it_audit_form.html template with company and audit data Rendered it_audit_form.html template with company and audit data
""" """
if not is_audit_owner():
abort(404)
db = SessionLocal() db = SessionLocal()
try: try:
from database import ITAudit from database import ITAudit
@ -136,6 +139,8 @@ def it_audit_save():
Rate limited to 30 requests per hour per user. 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 # Apply rate limiting manually since decorator doesn't work with blueprint
limiter = get_limiter() limiter = get_limiter()
if limiter: if limiter:

View File

@ -9,8 +9,9 @@ import csv
import logging import logging
from io import StringIO 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 flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import SessionLocal, Company from database import SessionLocal, Company
from . import bp from . import bp
@ -43,6 +44,8 @@ def api_it_audit_matches(company_id):
- partner company info (id, name, slug) - partner company info (id, name, slug)
- match_reason and shared_attributes - match_reason and shared_attributes
""" """
if not is_audit_owner():
abort(404)
# Only users with admin panel access can view collaboration matches # Only users with admin panel access can view collaboration matches
if not current_user.can_access_admin_panel(): if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
@ -136,6 +139,8 @@ def api_it_audit_history(company_id):
- audit_id, audit_date, overall_score, scores, maturity_level - audit_id, audit_date, overall_score, scores, maturity_level
- is_current flag (True for the most recent audit) - 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 from it_audit_service import get_company_audit_history
# Access control: users with company edit rights can view history # Access control: users with company edit rights can view history
@ -210,6 +215,8 @@ def api_it_audit_export():
Returns: Returns:
CSV file with IT audit data CSV file with IT audit data
""" """
if not is_audit_owner():
abort(404)
if not current_user.can_access_admin_panel(): if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,

View File

@ -0,0 +1,111 @@
{% extends "base.html" %}
{% block title %}Kontrola dostepu - Admin{% endblock %}
{% block content %}
<div class="admin-container">
<div class="page-header">
<h1>Kontrola dostepu</h1>
<p style="color: var(--text-secondary); margin-top: var(--spacing-xs);">
Matryca dostepu do funkcji audytowych
</p>
</div>
<!-- Rule explanation -->
<div class="card" style="margin-bottom: var(--spacing-lg); background: #eff6ff; border: 1px solid #bfdbfe;">
<div style="padding: var(--spacing-md);">
<h3 style="margin: 0 0 var(--spacing-sm) 0; color: #1e40af;">Zasada ograniczenia dostepu</h3>
<p style="margin: 0; color: #1e3a5f;">
Audyty SEO, IT, GBP i Social Media sa widoczne wylacznie dla <strong>{{ audit_owner_email }}</strong>.
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+.
</p>
</div>
</div>
<!-- Access matrix -->
<div class="card">
<div style="padding: var(--spacing-md); overflow-x: auto;">
<table class="data-table" style="width: 100%;">
<thead>
<tr>
<th>Uzytkownik</th>
<th>Email</th>
<th>Rola</th>
<th style="text-align: center;">Audyt SEO</th>
<th style="text-align: center;">Audyt IT</th>
<th style="text-align: center;">Audyt GBP</th>
<th style="text-align: center;">Audyt Social</th>
<th style="text-align: center;">Audyt KRS</th>
<th style="text-align: center;">Digital Maturity</th>
<th style="text-align: center;">Inne admin</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td><strong>{{ user.name or 'Brak nazwy' }}</strong></td>
<td>{{ user.email }}</td>
<td>
<span class="badge badge-{{ 'success' if user.role == 'ADMIN' else 'info' }}">
{{ user.role }}
</span>
</td>
{% set is_owner = user.email == audit_owner_email %}
<td style="text-align: center;">
{% if is_owner %}
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
{% else %}
<span style="color: #dc2626;" title="Brak dostepu">&#10007;</span>
{% endif %}
</td>
<td style="text-align: center;">
{% if is_owner %}
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
{% else %}
<span style="color: #dc2626;" title="Brak dostepu">&#10007;</span>
{% endif %}
</td>
<td style="text-align: center;">
{% if is_owner %}
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
{% else %}
<span style="color: #dc2626;" title="Brak dostepu">&#10007;</span>
{% endif %}
</td>
<td style="text-align: center;">
{% if is_owner %}
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
{% else %}
<span style="color: #dc2626;" title="Brak dostepu">&#10007;</span>
{% endif %}
</td>
<td style="text-align: center;">
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
</td>
<td style="text-align: center;">
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
</td>
<td style="text-align: center;">
<span style="color: #16a34a; font-weight: bold;" title="Dostep">&#10003;</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Reversibility note -->
<div class="card" style="margin-top: var(--spacing-lg); background: #fefce8; border: 1px solid #fde68a;">
<div style="padding: var(--spacing-md);">
<h3 style="margin: 0 0 var(--spacing-sm) 0; color: #92400e;">Odwracalnosc</h3>
<p style="margin: 0; color: #78350f;">
Aby przywrocic dostep do audytow dla wszystkich administratorow,
nalezy zmienic funkcje <code>is_audit_owner()</code> w pliku
<code>utils/decorators.py</code> na sprawdzanie roli OFFICE_MANAGER.
</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1518,6 +1518,7 @@
</svg> </svg>
</button> </button>
<div class="admin-dropdown-menu"> <div class="admin-dropdown-menu">
{% if is_audit_owner %}
<a href="{{ url_for('admin.admin_seo') }}"> <a href="{{ url_for('admin.admin_seo') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
@ -1543,6 +1544,13 @@
</svg> </svg>
Audyt Social Audyt Social
</a> </a>
<a href="{{ url_for('admin.admin_access_overview') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
Kontrola dostepu
</a>
{% endif %}
<a href="{{ url_for('admin.admin_krs_audit') }}"> <a href="{{ url_for('admin.admin_krs_audit') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>

View File

@ -818,7 +818,7 @@
</div> </div>
{# Audit links - separate row, visible only to authorized users #} {# 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 %}
<div class="contact-bar" style="margin-top: var(--spacing-sm, 8px);"> <div class="contact-bar" style="margin-top: var(--spacing-sm, 8px);">
<a href="{{ url_for('gbp_audit_dashboard', slug=company.slug) }}" class="contact-bar-item gbp-audit" title="Audyt Google Business Profile"> <a href="{{ url_for('gbp_audit_dashboard', slug=company.slug) }}" class="contact-bar-item gbp-audit" title="Audyt Google Business Profile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -2747,7 +2747,7 @@
{% endif %} {% endif %}
<!-- SEO Metrics Section - Only show if SEO audit was performed --> <!-- SEO Metrics Section - Only show if SEO audit was performed -->
{% if website_analysis and website_analysis.seo_audited_at %} {% if website_analysis and website_analysis.seo_audited_at and is_audit_owner %}
<div class="company-section" id="seo-metrics"> <div class="company-section" id="seo-metrics">
<h2 class="section-title"> <h2 class="section-title">
Analiza SEO Analiza SEO
@ -3078,7 +3078,7 @@
{% endif %} {% endif %}
<!-- GBP Audit Section - Only show if GBP audit was performed --> <!-- GBP Audit Section - Only show if GBP audit was performed -->
{% 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 %}
<div class="company-section" id="gbp-audit"> <div class="company-section" id="gbp-audit">
<h2 class="section-title"> <h2 class="section-title">
Audyt Google Business Profile Audyt Google Business Profile
@ -3291,7 +3291,7 @@
{% endif %} {% endif %}
<!-- Social Media Audit Section --> <!-- Social Media Audit Section -->
{% if social_media and social_media|length > 0 %} {% if social_media and social_media|length > 0 and is_audit_owner %}
<div class="company-section" id="social-media-audit"> <div class="company-section" id="social-media-audit">
<h2 class="section-title"> <h2 class="section-title">
Audyt Social Media Audyt Social Media
@ -3362,7 +3362,7 @@
{% endif %} {% endif %}
<!-- IT Audit Section - Only show if IT audit was performed --> <!-- IT Audit Section - Only show if IT audit was performed -->
{% 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 %}
<div class="company-section" id="it-audit"> <div class="company-section" id="it-audit">
<h2 class="section-title"> <h2 class="section-title">
Audyt IT Audyt IT

View File

@ -18,6 +18,15 @@ from functools import wraps
from flask import abort, flash, redirect, url_for, request from flask import abort, flash, redirect, url_for, request
from flask_login import current_user 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) # Import role enums (lazy import to avoid circular dependencies)
def _get_system_role(): def _get_system_role():
from database import SystemRole from database import SystemRole