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
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:
parent
e8b7f2214f
commit
7f77d7ebcd
6
app.py
6
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
|
@app.context_processor
|
||||||
def inject_notifications():
|
def inject_notifications():
|
||||||
"""Inject unread notifications count into all templates"""
|
"""Inject unread notifications count into all templates"""
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
111
templates/admin/access_overview.html
Normal file
111
templates/admin/access_overview.html
Normal 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">✓</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #dc2626;" title="Brak dostepu">✗</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
{% if is_owner %}
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #dc2626;" title="Brak dostepu">✗</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
{% if is_owner %}
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #dc2626;" title="Brak dostepu">✗</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
{% if is_owner %}
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #dc2626;" title="Brak dostepu">✗</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<span style="color: #16a34a; font-weight: bold;" title="Dostep">✓</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 %}
|
||||||
@ -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"/>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user