refactor: Migrate access control from is_admin to role-based system

Replace ~170 manual `if not current_user.is_admin` checks with:
- @role_required(SystemRole.ADMIN) for user management, security, ZOPK
- @role_required(SystemRole.OFFICE_MANAGER) for content management
- current_user.can_access_admin_panel() for admin UI access
- current_user.can_moderate_forum() for forum moderation
- current_user.can_edit_company(id) for company permissions

Add @office_manager_required decorator shortcut.
Add SQL migration to sync existing users' role field.

Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-01 21:05:22 +01:00
parent d90b7ec3b7
commit 4181a2e760
47 changed files with 421 additions and 635 deletions

2
app.py
View File

@ -912,7 +912,7 @@ def health():
@login_required @login_required
def test_error_500(): def test_error_500():
"""Test endpoint to trigger 500 error for notification testing. Admin only.""" """Test endpoint to trigger 500 error for notification testing. Admin only."""
if not current_user.is_admin: if not current_user.can_access_admin_panel():
flash('Brak uprawnień', 'error') flash('Brak uprawnień', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
# Intentionally raise an error to test error notification # Intentionally raise an error to test error notification

View File

@ -3,5 +3,12 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. --> <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity* ### Jan 31, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #250 | 6:33 PM | 🔵 | Nordabiz admin blueprint imports 14 separate routes modules demonstrating extreme modularization | ~677 |
| #180 | 6:25 PM | 🔵 | Nordabiz project architecture analyzed revealing 16+ Flask blueprints with modular organization | ~831 |
| #170 | 6:23 PM | 🔵 | Nordabiz admin routes handle recommendations moderation with Polish localization | ~713 |
| #168 | " | 🔵 | Nordabiz admin blueprint splits functionality across 15 route modules | ~726 |
</claude-mem-context> </claude-mem-context>

View File

@ -23,8 +23,10 @@ from werkzeug.security import generate_password_hash
from . import bp from . import bp
from database import ( from database import (
SessionLocal, User, Company, CompanyRecommendation, SessionLocal, User, Company, CompanyRecommendation,
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee,
SystemRole
) )
from utils.decorators import role_required
import gemini_service import gemini_service
# Logger # Logger
@ -44,12 +46,9 @@ MONTHS_PL = [
@bp.route('/recommendations') @bp.route('/recommendations')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendations(): def admin_recommendations():
"""Admin panel for recommendations moderation""" """Admin panel for recommendations moderation"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
recommendations = db.query(CompanyRecommendation).order_by( recommendations = db.query(CompanyRecommendation).order_by(
@ -86,11 +85,9 @@ def admin_recommendations():
@bp.route('/recommendations/<int:recommendation_id>/approve', methods=['POST']) @bp.route('/recommendations/<int:recommendation_id>/approve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendation_approve(recommendation_id): def admin_recommendation_approve(recommendation_id):
"""Approve a recommendation""" """Approve a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
recommendation = db.query(CompanyRecommendation).filter( recommendation = db.query(CompanyRecommendation).filter(
@ -117,11 +114,9 @@ def admin_recommendation_approve(recommendation_id):
@bp.route('/recommendations/<int:recommendation_id>/reject', methods=['POST']) @bp.route('/recommendations/<int:recommendation_id>/reject', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendation_reject(recommendation_id): def admin_recommendation_reject(recommendation_id):
"""Reject a recommendation""" """Reject a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
recommendation = db.query(CompanyRecommendation).filter( recommendation = db.query(CompanyRecommendation).filter(
@ -154,12 +149,9 @@ def admin_recommendation_reject(recommendation_id):
@bp.route('/users') @bp.route('/users')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_users(): def admin_users():
"""Admin panel for user management""" """Admin panel for user management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
users = db.query(User).order_by(User.created_at.desc()).all() users = db.query(User).order_by(User.created_at.desc()).all()
@ -187,11 +179,9 @@ def admin_users():
@bp.route('/users/add', methods=['POST']) @bp.route('/users/add', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_add(): def admin_user_add():
"""Create a new user (admin only)""" """Create a new user (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -241,11 +231,9 @@ def admin_user_add():
@bp.route('/users/<int:user_id>/toggle-admin', methods=['POST']) @bp.route('/users/<int:user_id>/toggle-admin', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_toggle_admin(user_id): def admin_user_toggle_admin(user_id):
"""Toggle admin status for a user""" """Toggle admin status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
if user_id == current_user.id: if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz zmienić własnych uprawnień'}), 400 return jsonify({'success': False, 'error': 'Nie możesz zmienić własnych uprawnień'}), 400
@ -271,11 +259,9 @@ def admin_user_toggle_admin(user_id):
@bp.route('/users/<int:user_id>/toggle-verified', methods=['POST']) @bp.route('/users/<int:user_id>/toggle-verified', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_toggle_verified(user_id): def admin_user_toggle_verified(user_id):
"""Toggle verified status for a user""" """Toggle verified status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -302,11 +288,9 @@ def admin_user_toggle_verified(user_id):
@bp.route('/users/<int:user_id>/update', methods=['POST']) @bp.route('/users/<int:user_id>/update', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_update(user_id): def admin_user_update(user_id):
"""Update user data (name, email)""" """Update user data (name, email)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -353,11 +337,9 @@ def admin_user_update(user_id):
@bp.route('/users/<int:user_id>/assign-company', methods=['POST']) @bp.route('/users/<int:user_id>/assign-company', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_assign_company(user_id): def admin_user_assign_company(user_id):
"""Assign a company to a user""" """Assign a company to a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -392,11 +374,9 @@ def admin_user_assign_company(user_id):
@bp.route('/users/<int:user_id>/delete', methods=['POST']) @bp.route('/users/<int:user_id>/delete', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_delete(user_id): def admin_user_delete(user_id):
"""Delete a user""" """Delete a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
if user_id == current_user.id: if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz usunąć własnego konta'}), 400 return jsonify({'success': False, 'error': 'Nie możesz usunąć własnego konta'}), 400
@ -422,11 +402,9 @@ def admin_user_delete(user_id):
@bp.route('/users/<int:user_id>/reset-password', methods=['POST']) @bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_user_reset_password(user_id): def admin_user_reset_password(user_id):
"""Generate password reset token for a user""" """Generate password reset token for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -458,12 +436,9 @@ def admin_user_reset_password(user_id):
@bp.route('/fees') @bp.route('/fees')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees(): def admin_fees():
"""Admin panel for membership fee management""" """Admin panel for membership fee management"""
if not current_user.is_admin:
flash('Brak uprawnien do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func, case from sqlalchemy import func, case
@ -545,11 +520,9 @@ def admin_fees():
@bp.route('/fees/generate', methods=['POST']) @bp.route('/fees/generate', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_generate(): def admin_fees_generate():
"""Generate fee records for all companies for a given month""" """Generate fee records for all companies for a given month"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
year = request.form.get('year', type=int) year = request.form.get('year', type=int)
@ -601,11 +574,9 @@ def admin_fees_generate():
@bp.route('/fees/<int:fee_id>/mark-paid', methods=['POST']) @bp.route('/fees/<int:fee_id>/mark-paid', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_mark_paid(fee_id): def admin_fees_mark_paid(fee_id):
"""Mark a fee as paid""" """Mark a fee as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
fee = db.query(MembershipFee).filter(MembershipFee.id == fee_id).first() fee = db.query(MembershipFee).filter(MembershipFee.id == fee_id).first()
@ -647,11 +618,9 @@ def admin_fees_mark_paid(fee_id):
@bp.route('/fees/bulk-mark-paid', methods=['POST']) @bp.route('/fees/bulk-mark-paid', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_bulk_mark_paid(): def admin_fees_bulk_mark_paid():
"""Bulk mark fees as paid""" """Bulk mark fees as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
fee_ids = request.form.getlist('fee_ids[]', type=int) fee_ids = request.form.getlist('fee_ids[]', type=int)
@ -686,12 +655,9 @@ def admin_fees_bulk_mark_paid():
@bp.route('/fees/export') @bp.route('/fees/export')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_export(): def admin_fees_export():
"""Export fees to CSV""" """Export fees to CSV"""
if not current_user.is_admin:
flash('Brak uprawnien.', 'error')
return redirect(url_for('.admin_fees'))
db = SessionLocal() db = SessionLocal()
try: try:
year = request.args.get('year', datetime.now().year, type=int) year = request.args.get('year', datetime.now().year, type=int)
@ -747,12 +713,9 @@ def admin_fees_export():
@bp.route('/kalendarz') @bp.route('/kalendarz')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar(): def admin_calendar():
"""Panel admin - zarządzanie wydarzeniami""" """Panel admin - zarządzanie wydarzeniami"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
db = SessionLocal() db = SessionLocal()
try: try:
events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).all() events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).all()
@ -764,12 +727,9 @@ def admin_calendar():
@bp.route('/kalendarz/nowy', methods=['GET', 'POST']) @bp.route('/kalendarz/nowy', methods=['GET', 'POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar_new(): def admin_calendar_new():
"""Dodaj nowe wydarzenie""" """Dodaj nowe wydarzenie"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
if request.method == 'POST': if request.method == 'POST':
db = SessionLocal() db = SessionLocal()
try: try:
@ -803,11 +763,9 @@ def admin_calendar_new():
@bp.route('/kalendarz/<int:event_id>/delete', methods=['POST']) @bp.route('/kalendarz/<int:event_id>/delete', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar_delete(event_id): def admin_calendar_delete(event_id):
"""Usuń wydarzenie""" """Usuń wydarzenie"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
event = db.query(NordaEvent).filter(NordaEvent.id == event_id).first() event = db.query(NordaEvent).filter(NordaEvent.id == event_id).first()
@ -834,14 +792,12 @@ def admin_calendar_delete(event_id):
@bp.route('/notify-release', methods=['POST']) @bp.route('/notify-release', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_notify_release(): def admin_notify_release():
""" """
Send notifications to all users about a new release. Send notifications to all users about a new release.
Called manually by admin after deploying a new version. Called manually by admin after deploying a new version.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
version = data.get('version') version = data.get('version')
highlights = data.get('highlights', []) highlights = data.get('highlights', [])

View File

@ -18,8 +18,10 @@ from sqlalchemy.orm import joinedload
from . import bp from . import bp
from database import ( from database import (
SessionLocal, User, UserSession, PageView, SearchQuery, SessionLocal, User, UserSession, PageView, SearchQuery,
ConversionEvent, JSError, Company, AIUsageLog, AIUsageDaily ConversionEvent, JSError, Company, AIUsageLog, AIUsageDaily,
SystemRole
) )
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,12 +32,9 @@ logger = logging.getLogger(__name__)
@bp.route('/analytics') @bp.route('/analytics')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_analytics(): def admin_analytics():
"""Admin dashboard for user analytics - sessions, page views, clicks""" """Admin dashboard for user analytics - sessions, page views, clicks"""
if not current_user.is_admin:
flash('Brak uprawnien do tej strony.', 'error')
return redirect(url_for('dashboard'))
period = request.args.get('period', 'week') period = request.args.get('period', 'week')
user_id = request.args.get('user_id', type=int) user_id = request.args.get('user_id', type=int)
@ -266,12 +265,9 @@ def admin_analytics():
@bp.route('/analytics/export') @bp.route('/analytics/export')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_analytics_export(): def admin_analytics_export():
"""Export analytics data as CSV""" """Export analytics data as CSV"""
if not current_user.is_admin:
flash('Brak uprawnien.', 'error')
return redirect(url_for('dashboard'))
export_type = request.args.get('type', 'sessions') export_type = request.args.get('type', 'sessions')
period = request.args.get('period', 'month') period = request.args.get('period', 'month')
@ -373,12 +369,9 @@ def admin_analytics_export():
@bp.route('/ai-usage') @bp.route('/ai-usage')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_ai_usage(): def admin_ai_usage():
"""Admin dashboard for AI (Gemini) API usage monitoring""" """Admin dashboard for AI (Gemini) API usage monitoring"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
from datetime import datetime, timedelta from datetime import datetime, timedelta
# Get period filter from query params # Get period filter from query params
@ -601,12 +594,9 @@ def admin_ai_usage():
@bp.route('/ai-usage/user/<int:user_id>') @bp.route('/ai-usage/user/<int:user_id>')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_ai_usage_user(user_id): def admin_ai_usage_user(user_id):
"""Detailed AI usage for a specific user""" """Detailed AI usage for a specific user"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Get user info # Get user info

View File

@ -13,7 +13,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from . import bp from . import bp
from database import SessionLocal, Announcement from database import SessionLocal, Announcement, SystemRole
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,12 +49,9 @@ def generate_slug(title):
@bp.route('/announcements') @bp.route('/announcements')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements(): def admin_announcements():
"""Admin panel - lista ogłoszeń""" """Admin panel - lista ogłoszeń"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Filters # Filters
@ -92,12 +90,9 @@ def admin_announcements():
@bp.route('/announcements/new', methods=['GET', 'POST']) @bp.route('/announcements/new', methods=['GET', 'POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_new(): def admin_announcements_new():
"""Admin panel - nowe ogłoszenie""" """Admin panel - nowe ogłoszenie"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
if request.method == 'POST': if request.method == 'POST':
db = SessionLocal() db = SessionLocal()
try: try:
@ -174,12 +169,9 @@ def admin_announcements_new():
@bp.route('/announcements/<int:id>/edit', methods=['GET', 'POST']) @bp.route('/announcements/<int:id>/edit', methods=['GET', 'POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_edit(id): def admin_announcements_edit(id):
"""Admin panel - edycja ogłoszenia""" """Admin panel - edycja ogłoszenia"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
announcement = db.query(Announcement).filter(Announcement.id == id).first() announcement = db.query(Announcement).filter(Announcement.id == id).first()
@ -259,11 +251,9 @@ def admin_announcements_edit(id):
@bp.route('/announcements/<int:id>/publish', methods=['POST']) @bp.route('/announcements/<int:id>/publish', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_publish(id): def admin_announcements_publish(id):
"""Publikacja ogłoszenia""" """Publikacja ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
announcement = db.query(Announcement).filter(Announcement.id == id).first() announcement = db.query(Announcement).filter(Announcement.id == id).first()
@ -300,11 +290,9 @@ def admin_announcements_publish(id):
@bp.route('/announcements/<int:id>/archive', methods=['POST']) @bp.route('/announcements/<int:id>/archive', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_archive(id): def admin_announcements_archive(id):
"""Archiwizacja ogłoszenia""" """Archiwizacja ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
announcement = db.query(Announcement).filter(Announcement.id == id).first() announcement = db.query(Announcement).filter(Announcement.id == id).first()
@ -328,11 +316,9 @@ def admin_announcements_archive(id):
@bp.route('/announcements/<int:id>/delete', methods=['POST']) @bp.route('/announcements/<int:id>/delete', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_delete(id): def admin_announcements_delete(id):
"""Usunięcie ogłoszenia""" """Usunięcie ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
announcement = db.query(Announcement).filter(Announcement.id == id).first() announcement = db.query(Announcement).filter(Announcement.id == id).first()

View File

@ -15,8 +15,9 @@ from . import bp
from database import ( from database import (
SessionLocal, Company, Category, CompanyWebsiteAnalysis, GBPAudit, SessionLocal, Company, Category, CompanyWebsiteAnalysis, GBPAudit,
CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson, CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson,
ITAudit, ITCollaborationMatch ITAudit, ITCollaborationMatch, SystemRole
) )
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -27,6 +28,7 @@ logger = logging.getLogger(__name__)
@bp.route('/seo') @bp.route('/seo')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_seo(): def admin_seo():
""" """
Admin dashboard for SEO metrics overview. Admin dashboard for SEO metrics overview.
@ -42,10 +44,6 @@ 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 current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
# 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', '')
@ -145,6 +143,7 @@ def admin_seo():
@bp.route('/gbp-audit') @bp.route('/gbp-audit')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_gbp_audit(): def admin_gbp_audit():
""" """
Admin dashboard for GBP (Google Business Profile) audit overview. Admin dashboard for GBP (Google Business Profile) audit overview.
@ -155,10 +154,6 @@ def admin_gbp_audit():
- Review metrics (avg rating, review counts) - Review metrics (avg rating, review counts)
- Photo statistics - Photo statistics
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func from sqlalchemy import func
@ -309,12 +304,9 @@ def admin_gbp_audit():
@bp.route('/digital-maturity') @bp.route('/digital-maturity')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def digital_maturity_dashboard(): def digital_maturity_dashboard():
"""Admin dashboard for digital maturity assessment results""" """Admin dashboard for digital maturity assessment results"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func, desc from sqlalchemy import func, desc
@ -386,6 +378,7 @@ def digital_maturity_dashboard():
@bp.route('/krs-audit') @bp.route('/krs-audit')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_krs_audit(): def admin_krs_audit():
""" """
Admin dashboard for KRS (Krajowy Rejestr Sądowy) audit. Admin dashboard for KRS (Krajowy Rejestr Sądowy) audit.
@ -396,10 +389,6 @@ def admin_krs_audit():
- Audit progress and status for each company - Audit progress and status for each company
- Links to source PDF files - Links to source PDF files
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
# Check if KRS audit service is available # Check if KRS audit service is available
try: try:
from krs_audit_service import KRS_AUDIT_AVAILABLE from krs_audit_service import KRS_AUDIT_AVAILABLE
@ -496,6 +485,7 @@ def admin_krs_audit():
@bp.route('/it-audit') @bp.route('/it-audit')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_it_audit(): def admin_it_audit():
""" """
Admin dashboard for IT audit overview. Admin dashboard for IT audit overview.
@ -507,12 +497,8 @@ def admin_it_audit():
- Company table with IT audit data - Company table with IT audit data
- Collaboration matches matrix - Collaboration matches matrix
Access: Admin only Access: Office Manager and above
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func from sqlalchemy import func

View File

@ -15,7 +15,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify, R
from flask_login import login_required, current_user from flask_login import login_required, current_user
from . import bp from . import bp
from database import SessionLocal, Company, Category, User, Person, CompanyPerson from database import SessionLocal, Company, Category, User, Person, CompanyPerson, SystemRole
from utils.decorators import role_required
# Logger # Logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,12 +38,9 @@ def validate_nip(nip: str) -> bool:
@bp.route('/companies') @bp.route('/companies')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_companies(): def admin_companies():
"""Admin panel for company management""" """Admin panel for company management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
# Get filter parameters # Get filter parameters
@ -104,11 +102,9 @@ def admin_companies():
@bp.route('/companies/add', methods=['POST']) @bp.route('/companies/add', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_add(): def admin_company_add():
"""Create a new company""" """Create a new company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -174,11 +170,9 @@ def admin_company_add():
@bp.route('/companies/<int:company_id>') @bp.route('/companies/<int:company_id>')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_get(company_id): def admin_company_get(company_id):
"""Get company details (JSON)""" """Get company details (JSON)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -207,11 +201,9 @@ def admin_company_get(company_id):
@bp.route('/companies/<int:company_id>/update', methods=['POST']) @bp.route('/companies/<int:company_id>/update', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_update(company_id): def admin_company_update(company_id):
"""Update company data""" """Update company data"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -278,11 +270,9 @@ def admin_company_update(company_id):
@bp.route('/companies/<int:company_id>/toggle-status', methods=['POST']) @bp.route('/companies/<int:company_id>/toggle-status', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_toggle_status(company_id): def admin_company_toggle_status(company_id):
"""Toggle company status (active <-> inactive)""" """Toggle company status (active <-> inactive)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -310,11 +300,9 @@ def admin_company_toggle_status(company_id):
@bp.route('/companies/<int:company_id>/delete', methods=['POST']) @bp.route('/companies/<int:company_id>/delete', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_delete(company_id): def admin_company_delete(company_id):
"""Soft delete company (set status to archived)""" """Soft delete company (set status to archived)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -337,11 +325,9 @@ def admin_company_delete(company_id):
@bp.route('/companies/<int:company_id>/assign-user', methods=['POST']) @bp.route('/companies/<int:company_id>/assign-user', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_assign_user(company_id): def admin_company_assign_user(company_id):
"""Assign a user to a company""" """Assign a user to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -373,11 +359,9 @@ def admin_company_assign_user(company_id):
@bp.route('/companies/<int:company_id>/people') @bp.route('/companies/<int:company_id>/people')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_people(company_id): def admin_company_people(company_id):
"""Get people associated with a company""" """Get people associated with a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -414,11 +398,9 @@ def admin_company_people(company_id):
@bp.route('/companies/<int:company_id>/users') @bp.route('/companies/<int:company_id>/users')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_users(company_id): def admin_company_users(company_id):
"""Get users assigned to a company""" """Get users assigned to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
company = db.query(Company).filter(Company.id == company_id).first() company = db.query(Company).filter(Company.id == company_id).first()
@ -446,12 +428,9 @@ def admin_company_users(company_id):
@bp.route('/companies/export') @bp.route('/companies/export')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_companies_export(): def admin_companies_export():
"""Export companies to CSV""" """Export companies to CSV"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
companies = db.query(Company).order_by(Company.name).all() companies = db.query(Company).order_by(Company.name).all()

View File

@ -10,6 +10,8 @@ import logging
from flask import render_template, request, redirect, url_for, flash, jsonify from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from database import SystemRole
from utils.decorators import role_required
from . import bp from . import bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,22 +23,17 @@ logger = logging.getLogger(__name__)
@bp.route('/insights') @bp.route('/insights')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_insights(): def admin_insights():
"""Admin dashboard for development insights from forum and chat""" """Admin dashboard for development insights from forum and chat"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
return render_template('admin/insights.html') return render_template('admin/insights.html')
@bp.route('/insights-api', methods=['GET']) @bp.route('/insights-api', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_get_insights(): def api_get_insights():
"""Get development insights for roadmap""" """Get development insights for roadmap"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
from norda_knowledge_service import get_knowledge_service from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service() service = get_knowledge_service()
@ -61,11 +58,9 @@ def api_get_insights():
@bp.route('/insights-api/<int:insight_id>/status', methods=['PUT']) @bp.route('/insights-api/<int:insight_id>/status', methods=['PUT'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_update_insight_status(insight_id): def api_update_insight_status(insight_id):
"""Update insight status (for roadmap planning)""" """Update insight status (for roadmap planning)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
from norda_knowledge_service import get_knowledge_service from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service() service = get_knowledge_service()
@ -87,11 +82,9 @@ def api_update_insight_status(insight_id):
@bp.route('/insights-api/sync', methods=['POST']) @bp.route('/insights-api/sync', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_sync_insights(): def api_sync_insights():
"""Manually trigger knowledge sync from forum and chat""" """Manually trigger knowledge sync from forum and chat"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
from norda_knowledge_service import get_knowledge_service from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service() service = get_knowledge_service()
@ -121,11 +114,9 @@ def api_sync_insights():
@bp.route('/insights-api/stats', methods=['GET']) @bp.route('/insights-api/stats', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_insights_stats(): def api_insights_stats():
"""Get knowledge base statistics""" """Get knowledge base statistics"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
from norda_knowledge_service import get_knowledge_service from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service() service = get_knowledge_service()
@ -152,11 +143,9 @@ def api_insights_stats():
@bp.route('/ai-learning-status') @bp.route('/ai-learning-status')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_ai_learning_status(): def api_ai_learning_status():
"""API: Get AI feedback learning status and examples""" """API: Get AI feedback learning status and examples"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
try: try:
from feedback_learning_service import get_feedback_learning_service from feedback_learning_service import get_feedback_learning_service
service = get_feedback_learning_service() service = get_feedback_learning_service()
@ -211,11 +200,9 @@ def api_ai_learning_status():
@bp.route('/chat-stats') @bp.route('/chat-stats')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_chat_stats(): def api_chat_stats():
"""API: Get chat statistics for dashboard""" """API: Get chat statistics for dashboard"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from datetime import datetime, timedelta from datetime import datetime, timedelta
from database import SessionLocal, AIChatMessage from database import SessionLocal, AIChatMessage

View File

@ -19,8 +19,10 @@ from database import (
CompanyPerson, CompanyPerson,
CompanyPKD, CompanyPKD,
CompanyFinancialReport, CompanyFinancialReport,
KRSAudit KRSAudit,
SystemRole
) )
from utils.decorators import role_required
from . import bp from . import bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -116,6 +118,7 @@ def _import_krs_person(db, company_id, person_data, role_category, source_docume
@bp.route('/krs-api/audit', methods=['POST']) @bp.route('/krs-api/audit', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_trigger(): def api_krs_audit_trigger():
""" """
API: Trigger KRS audit for a company (admin-only). API: Trigger KRS audit for a company (admin-only).
@ -134,12 +137,6 @@ def api_krs_audit_trigger():
- Success: Audit results saved to database - Success: Audit results saved to database
- Error: Error message with status code - Error: Error message with status code
""" """
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Brak uprawnień. Tylko administrator może uruchamiać audyty KRS.'
}), 403
if not is_krs_audit_available(): if not is_krs_audit_available():
return jsonify({ return jsonify({
'success': False, 'success': False,
@ -350,6 +347,7 @@ def api_krs_audit_trigger():
@bp.route('/krs-api/audit/batch', methods=['POST']) @bp.route('/krs-api/audit/batch', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_batch(): def api_krs_audit_batch():
""" """
API: Trigger batch KRS audit for all companies with KRS numbers. API: Trigger batch KRS audit for all companies with KRS numbers.
@ -357,12 +355,6 @@ def api_krs_audit_batch():
This runs audits sequentially to avoid overloading the system. This runs audits sequentially to avoid overloading the system.
Returns progress updates via the response. Returns progress updates via the response.
""" """
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Brak uprawnień.'
}), 403
if not is_krs_audit_available(): if not is_krs_audit_available():
return jsonify({ return jsonify({
'success': False, 'success': False,

View File

@ -16,9 +16,11 @@ from sqlalchemy.orm.attributes import flag_modified
from . import bp from . import bp
from database import ( from database import (
SessionLocal, MembershipApplication, CompanyDataRequest, SessionLocal, MembershipApplication, CompanyDataRequest,
Company, Category, User, UserNotification, Person, CompanyPerson, CompanyPKD Company, Category, User, UserNotification, Person, CompanyPerson, CompanyPKD,
SystemRole
) )
from krs_api_service import get_company_from_krs from krs_api_service import get_company_from_krs
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -156,12 +158,9 @@ def _enrich_company_from_krs(company, db):
@bp.route('/membership') @bp.route('/membership')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership(): def admin_membership():
"""Admin panel for membership applications.""" """Admin panel for membership applications."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
# Get filter parameters # Get filter parameters
@ -224,12 +223,9 @@ def admin_membership():
@bp.route('/membership/<int:app_id>') @bp.route('/membership/<int:app_id>')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_detail(app_id): def admin_membership_detail(app_id):
"""View membership application details.""" """View membership application details."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -253,11 +249,9 @@ def admin_membership_detail(app_id):
@bp.route('/membership/<int:app_id>/approve', methods=['POST']) @bp.route('/membership/<int:app_id>/approve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_approve(app_id): def admin_membership_approve(app_id):
"""Approve membership application and create company.""" """Approve membership application and create company."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -398,11 +392,9 @@ def admin_membership_approve(app_id):
@bp.route('/membership/<int:app_id>/reject', methods=['POST']) @bp.route('/membership/<int:app_id>/reject', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_reject(app_id): def admin_membership_reject(app_id):
"""Reject membership application.""" """Reject membership application."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -444,11 +436,9 @@ def admin_membership_reject(app_id):
@bp.route('/membership/<int:app_id>/request-changes', methods=['POST']) @bp.route('/membership/<int:app_id>/request-changes', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_request_changes(app_id): def admin_membership_request_changes(app_id):
"""Request changes to membership application.""" """Request changes to membership application."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -490,11 +480,9 @@ def admin_membership_request_changes(app_id):
@bp.route('/membership/<int:app_id>/start-review', methods=['POST']) @bp.route('/membership/<int:app_id>/start-review', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_start_review(app_id): def admin_membership_start_review(app_id):
"""Mark application as under review.""" """Mark application as under review."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -521,14 +509,12 @@ def admin_membership_start_review(app_id):
@bp.route('/membership/<int:app_id>/propose-changes', methods=['POST']) @bp.route('/membership/<int:app_id>/propose-changes', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_propose_changes(app_id): def admin_membership_propose_changes(app_id):
""" """
Propose changes from registry data for user approval. Propose changes from registry data for user approval.
Instead of directly updating, save proposed changes and notify user. Instead of directly updating, save proposed changes and notify user.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
application = db.query(MembershipApplication).get(app_id) application = db.query(MembershipApplication).get(app_id)
@ -651,15 +637,13 @@ def _get_field_label(field_name):
@bp.route('/membership/<int:app_id>/update-from-registry', methods=['POST']) @bp.route('/membership/<int:app_id>/update-from-registry', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_update_from_registry(app_id): def admin_membership_update_from_registry(app_id):
""" """
[DEPRECATED - use propose-changes instead] [DEPRECATED - use propose-changes instead]
Direct update is now replaced by propose-changes workflow. Direct update is now replaced by propose-changes workflow.
This endpoint is kept for backward compatibility but redirects to propose-changes. This endpoint is kept for backward compatibility but redirects to propose-changes.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Redirect to new workflow # Redirect to new workflow
data = request.get_json() or {} data = request.get_json() or {}
return admin_membership_propose_changes.__wrapped__(app_id) return admin_membership_propose_changes.__wrapped__(app_id)
@ -671,12 +655,9 @@ def admin_membership_update_from_registry(app_id):
@bp.route('/company-requests') @bp.route('/company-requests')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_requests(): def admin_company_requests():
"""Admin panel for company data requests.""" """Admin panel for company data requests."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
status_filter = request.args.get('status', 'pending') status_filter = request.args.get('status', 'pending')
@ -713,11 +694,9 @@ def admin_company_requests():
@bp.route('/company-requests/<int:req_id>/approve', methods=['POST']) @bp.route('/company-requests/<int:req_id>/approve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_request_approve(req_id): def admin_company_request_approve(req_id):
"""Approve company data request and update company.""" """Approve company data request and update company."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data_request = db.query(CompanyDataRequest).get(req_id) data_request = db.query(CompanyDataRequest).get(req_id)
@ -803,11 +782,9 @@ def admin_company_request_approve(req_id):
@bp.route('/company-requests/<int:req_id>/reject', methods=['POST']) @bp.route('/company-requests/<int:req_id>/reject', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_request_reject(req_id): def admin_company_request_reject(req_id):
"""Reject company data request.""" """Reject company data request."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data_request = db.query(CompanyDataRequest).get(req_id) data_request = db.query(CompanyDataRequest).get(req_id)

View File

@ -28,8 +28,10 @@ from database import (
GBPAudit, GBPAudit,
NordaEvent, NordaEvent,
SessionLocal, SessionLocal,
SystemRole,
ZOPKNews, ZOPKNews,
) )
from utils.decorators import role_required
from . import bp from . import bp
@ -38,11 +40,9 @@ logger = logging.getLogger(__name__)
@bp.route('/model-comparison') @bp.route('/model-comparison')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_model_comparison(): def admin_model_comparison():
"""Admin page for comparing AI model responses""" """Admin page for comparing AI model responses"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
# Load saved comparison results if exist # Load saved comparison results if exist
results_file = '/tmp/nordabiz_model_comparison_results.json' results_file = '/tmp/nordabiz_model_comparison_results.json'
@ -68,11 +68,9 @@ def admin_model_comparison():
@bp.route('/model-comparison/run', methods=['POST']) @bp.route('/model-comparison/run', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_model_comparison_run(): def admin_model_comparison_run():
"""Run model comparison simulation""" """Run model comparison simulation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try: try:
# Questions to compare (from real conversations) # Questions to compare (from real conversations)
comparison_questions = { comparison_questions = {

View File

@ -13,7 +13,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from . import bp from . import bp
from database import SessionLocal, Company, Person, CompanyPerson from database import SessionLocal, Company, Person, CompanyPerson, SystemRole
from utils.decorators import role_required
# Logger # Logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,12 +26,9 @@ logger = logging.getLogger(__name__)
@bp.route('/people') @bp.route('/people')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_people(): def admin_people():
"""Admin panel for person management""" """Admin panel for person management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal() db = SessionLocal()
try: try:
# Get search query # Get search query
@ -114,11 +112,9 @@ def admin_people():
@bp.route('/people/add', methods=['POST']) @bp.route('/people/add', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_add(): def admin_person_add():
"""Create a new person""" """Create a new person"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -171,11 +167,9 @@ def admin_person_add():
@bp.route('/people/<int:person_id>') @bp.route('/people/<int:person_id>')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_get(person_id): def admin_person_get(person_id):
"""Get person details (JSON)""" """Get person details (JSON)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
person = db.query(Person).filter(Person.id == person_id).first() person = db.query(Person).filter(Person.id == person_id).first()
@ -198,11 +192,9 @@ def admin_person_get(person_id):
@bp.route('/people/<int:person_id>/update', methods=['POST']) @bp.route('/people/<int:person_id>/update', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_update(person_id): def admin_person_update(person_id):
"""Update person data""" """Update person data"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
person = db.query(Person).filter(Person.id == person_id).first() person = db.query(Person).filter(Person.id == person_id).first()
@ -256,11 +248,9 @@ def admin_person_update(person_id):
@bp.route('/people/<int:person_id>/delete', methods=['POST']) @bp.route('/people/<int:person_id>/delete', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_delete(person_id): def admin_person_delete(person_id):
"""Delete person (hard delete with CASCADE on CompanyPerson)""" """Delete person (hard delete with CASCADE on CompanyPerson)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
person = db.query(Person).filter(Person.id == person_id).first() person = db.query(Person).filter(Person.id == person_id).first()
@ -286,11 +276,9 @@ def admin_person_delete(person_id):
@bp.route('/people/<int:person_id>/companies') @bp.route('/people/<int:person_id>/companies')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_companies(person_id): def admin_person_companies(person_id):
"""Get companies associated with a person""" """Get companies associated with a person"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
person = db.query(Person).filter(Person.id == person_id).first() person = db.query(Person).filter(Person.id == person_id).first()
@ -321,11 +309,9 @@ def admin_person_companies(person_id):
@bp.route('/people/<int:person_id>/link-company', methods=['POST']) @bp.route('/people/<int:person_id>/link-company', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_link_company(person_id): def admin_person_link_company(person_id):
"""Link person to a company""" """Link person to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -393,11 +379,9 @@ def admin_person_link_company(person_id):
@bp.route('/people/<int:person_id>/unlink-company/<int:company_id>', methods=['POST']) @bp.route('/people/<int:person_id>/unlink-company/<int:company_id>', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_unlink_company(person_id, company_id): def admin_person_unlink_company(person_id, company_id):
"""Remove person-company link""" """Remove person-company link"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -439,11 +423,9 @@ def admin_person_unlink_company(person_id, company_id):
@bp.route('/people/search') @bp.route('/people/search')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_people_search(): def admin_people_search():
"""Search people for autocomplete""" """Search people for autocomplete"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
query = request.args.get('q', '').strip() query = request.args.get('q', '').strip()

View File

@ -12,7 +12,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from . import bp from . import bp
from database import SessionLocal, User, AuditLog, SecurityAlert from database import SessionLocal, User, AuditLog, SecurityAlert, SystemRole
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,12 +35,9 @@ except ImportError:
@bp.route('/security') @bp.route('/security')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_security(): def admin_security():
"""Security dashboard - audit logs, alerts, GeoIP stats""" """Security dashboard - audit logs, alerts, GeoIP stats"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
from sqlalchemy import func, desc from sqlalchemy import func, desc
@ -158,11 +156,9 @@ def admin_security():
@bp.route('/security/alert/<int:alert_id>/acknowledge', methods=['POST']) @bp.route('/security/alert/<int:alert_id>/acknowledge', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def acknowledge_security_alert(alert_id): def acknowledge_security_alert(alert_id):
"""Acknowledge a security alert""" """Acknowledge a security alert"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
alert = db.query(SecurityAlert).get(alert_id) alert = db.query(SecurityAlert).get(alert_id)
@ -186,11 +182,9 @@ def acknowledge_security_alert(alert_id):
@bp.route('/security/alert/<int:alert_id>/resolve', methods=['POST']) @bp.route('/security/alert/<int:alert_id>/resolve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def resolve_security_alert(alert_id): def resolve_security_alert(alert_id):
"""Resolve a security alert""" """Resolve a security alert"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
note = request.form.get('note', '') note = request.form.get('note', '')
db = SessionLocal() db = SessionLocal()
@ -219,11 +213,9 @@ def resolve_security_alert(alert_id):
@bp.route('/security/unlock-account/<int:user_id>', methods=['POST']) @bp.route('/security/unlock-account/<int:user_id>', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def unlock_account(user_id): def unlock_account(user_id):
"""Unlock a locked user account""" """Unlock a locked user account"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
user = db.query(User).get(user_id) user = db.query(User).get(user_id)
@ -246,11 +238,9 @@ def unlock_account(user_id):
@bp.route('/security/geoip-stats') @bp.route('/security/geoip-stats')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_geoip_stats(): def api_geoip_stats():
"""API endpoint for GeoIP stats auto-refresh""" """API endpoint for GeoIP stats auto-refresh"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from sqlalchemy import func from sqlalchemy import func
db = SessionLocal() db = SessionLocal()

View File

@ -14,8 +14,9 @@ from sqlalchemy import func, distinct
from . import bp from . import bp
from database import ( from database import (
SessionLocal, Company, Category, CompanySocialMedia SessionLocal, Company, Category, CompanySocialMedia, SystemRole
) )
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,12 +27,9 @@ logger = logging.getLogger(__name__)
@bp.route('/social-media') @bp.route('/social-media')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_social_media(): def admin_social_media():
"""Admin dashboard for social media analytics""" """Admin dashboard for social media analytics"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Total counts per platform # Total counts per platform
@ -123,6 +121,7 @@ def admin_social_media():
@bp.route('/social-audit') @bp.route('/social-audit')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_social_audit(): def admin_social_audit():
""" """
Admin dashboard for Social Media audit overview. Admin dashboard for Social Media audit overview.
@ -133,10 +132,6 @@ 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 current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Platform definitions # Platform definitions

View File

@ -18,8 +18,9 @@ from sqlalchemy import func, text
from . import bp from . import bp
from database import ( from database import (
SessionLocal, Company, User, AuditLog, SecurityAlert, SessionLocal, Company, User, AuditLog, SecurityAlert,
CompanySocialMedia, CompanyWebsiteAnalysis CompanySocialMedia, CompanyWebsiteAnalysis, SystemRole
) )
from utils.decorators import role_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,12 +31,9 @@ logger = logging.getLogger(__name__)
@bp.route('/status') @bp.route('/status')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_status(): def admin_status():
"""System status dashboard with real-time metrics""" """System status dashboard with real-time metrics"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('public.dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Current timestamp # Current timestamp
@ -535,11 +533,9 @@ def admin_status():
@bp.route('/api/status') @bp.route('/api/status')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_admin_status(): def api_admin_status():
"""API endpoint for status dashboard auto-refresh""" """API endpoint for status dashboard auto-refresh"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
now = datetime.now() now = datetime.now()
@ -611,15 +607,12 @@ def api_admin_status():
@bp.route('/health') @bp.route('/health')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_health(): def admin_health():
""" """
Graphical health check dashboard. Graphical health check dashboard.
Shows status of all critical endpoints with visual indicators. Shows status of all critical endpoints with visual indicators.
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
from flask import current_app from flask import current_app
results = [] results = []
@ -748,11 +741,9 @@ def admin_health():
@bp.route('/api/health') @bp.route('/api/health')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_admin_health(): def api_admin_health():
"""API endpoint for health dashboard auto-refresh""" """API endpoint for health dashboard auto-refresh"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from flask import current_app from flask import current_app
# Run the same checks as admin_health but return JSON # Run the same checks as admin_health but return JSON
@ -803,21 +794,17 @@ def api_admin_health():
@bp.route('/debug') @bp.route('/debug')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def debug_panel(): def debug_panel():
"""Real-time debug panel for monitoring app activity""" """Real-time debug panel for monitoring app activity"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('public.dashboard'))
return render_template('admin/debug.html') return render_template('admin/debug.html')
@bp.route('/api/logs') @bp.route('/api/logs')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_get_logs(): def api_get_logs():
"""API: Get recent logs""" """API: Get recent logs"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
# Import debug_handler from main app # Import debug_handler from main app
from app import debug_handler from app import debug_handler
@ -848,11 +835,9 @@ def api_get_logs():
@bp.route('/api/logs/stream') @bp.route('/api/logs/stream')
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_logs_stream(): def api_logs_stream():
"""SSE endpoint for real-time log streaming""" """SSE endpoint for real-time log streaming"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from app import debug_handler from app import debug_handler
import time import time
@ -873,11 +858,9 @@ def api_logs_stream():
@bp.route('/api/logs/clear', methods=['POST']) @bp.route('/api/logs/clear', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_clear_logs(): def api_clear_logs():
"""API: Clear log buffer""" """API: Clear log buffer"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from app import debug_handler from app import debug_handler
debug_handler.logs.clear() debug_handler.logs.clear()
logger.info("Log buffer cleared by admin") logger.info("Log buffer cleared by admin")
@ -886,11 +869,9 @@ def api_clear_logs():
@bp.route('/api/test-log', methods=['POST']) @bp.route('/api/test-log', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_test_log(): def api_test_log():
"""API: Generate test log entries""" """API: Generate test log entries"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
logger.debug("Test DEBUG message") logger.debug("Test DEBUG message")
logger.info("Test INFO message") logger.info("Test INFO message")
logger.warning("Test WARNING message") logger.warning("Test WARNING message")

View File

@ -18,6 +18,7 @@ from flask_login import current_user, login_required
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from database import SessionLocal, User, Company, SystemRole, CompanyRole, UserCompanyPermissions from database import SessionLocal, User, Company, SystemRole, CompanyRole, UserCompanyPermissions
from utils.decorators import role_required
import gemini_service import gemini_service
from . import bp from . import bp
@ -107,11 +108,9 @@ ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed
@bp.route('/users-api/ai-parse', methods=['POST']) @bp.route('/users-api/ai-parse', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_users_ai_parse(): def admin_users_ai_parse():
"""Parse text or image with AI to extract user data.""" """Parse text or image with AI to extract user data."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
# Get list of companies for AI context # Get list of companies for AI context
@ -221,11 +220,9 @@ def admin_users_ai_parse():
@bp.route('/users-api/bulk-create', methods=['POST']) @bp.route('/users-api/bulk-create', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_users_bulk_create(): def admin_users_bulk_create():
"""Create multiple users from confirmed proposals.""" """Create multiple users from confirmed proposals."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -309,11 +306,9 @@ def admin_users_bulk_create():
@bp.route('/users-api/change-role', methods=['POST']) @bp.route('/users-api/change-role', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_users_change_role(): def admin_users_change_role():
"""Change user's system role.""" """Change user's system role."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
data = request.get_json() or {} data = request.get_json() or {}
@ -389,11 +384,9 @@ def admin_users_change_role():
@bp.route('/users-api/roles', methods=['GET']) @bp.route('/users-api/roles', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_users_get_roles(): def admin_users_get_roles():
"""Get list of available roles for dropdown.""" """Get list of available roles for dropdown."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
roles = [ roles = [
{'value': 'UNAFFILIATED', 'label': 'Niezrzeszony', 'description': 'Firma spoza Izby'}, {'value': 'UNAFFILIATED', 'label': 'Niezrzeszony', 'description': 'Firma spoza Izby'},
{'value': 'MEMBER', 'label': 'Członek', 'description': 'Członek Norda bez firmy'}, {'value': 'MEMBER', 'label': 'Członek', 'description': 'Członek Norda bez firmy'},

View File

@ -11,23 +11,22 @@ from flask_login import current_user, login_required
from database import ( from database import (
SessionLocal, SessionLocal,
SystemRole,
ZOPKProject, ZOPKProject,
ZOPKStakeholder, ZOPKStakeholder,
ZOPKNews, ZOPKNews,
ZOPKResource, ZOPKResource,
ZOPKNewsFetchJob ZOPKNewsFetchJob
) )
from utils.decorators import role_required
from . import bp from . import bp
@bp.route('/zopk') @bp.route('/zopk')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk(): def admin_zopk():
"""Admin dashboard for ZOPK management""" """Admin dashboard for ZOPK management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
# Pagination and filtering parameters # Pagination and filtering parameters

View File

@ -18,11 +18,13 @@ from sqlalchemy import text, func, distinct
from database import ( from database import (
SessionLocal, SessionLocal,
SystemRole,
ZOPKNews, ZOPKNews,
ZOPKKnowledgeChunk, ZOPKKnowledgeChunk,
ZOPKKnowledgeEntity, ZOPKKnowledgeEntity,
ZOPKKnowledgeEntityMention ZOPKKnowledgeEntityMention
) )
from utils.decorators import role_required
from . import bp from . import bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -35,6 +37,7 @@ _GRAPH_CACHE_TTL = 300 # 5 minutes
@bp.route('/zopk/knowledge/stats') @bp.route('/zopk/knowledge/stats')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_stats(): def admin_zopk_knowledge_stats():
""" """
Get knowledge extraction statistics. Get knowledge extraction statistics.
@ -44,9 +47,6 @@ def admin_zopk_knowledge_stats():
- knowledge_base: stats about chunks, facts, entities, relations - knowledge_base: stats about chunks, facts, entities, relations
- top_entities: most mentioned entities - top_entities: most mentioned entities
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import get_knowledge_stats from zopk_knowledge_service import get_knowledge_stats
db = SessionLocal() db = SessionLocal()
@ -65,6 +65,7 @@ def admin_zopk_knowledge_stats():
@bp.route('/zopk/knowledge/extract', methods=['POST']) @bp.route('/zopk/knowledge/extract', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract(): def admin_zopk_knowledge_extract():
""" """
Batch extract knowledge from scraped articles. Batch extract knowledge from scraped articles.
@ -77,9 +78,6 @@ def admin_zopk_knowledge_extract():
- chunks/facts/entities/relations created - chunks/facts/entities/relations created
- errors list - errors list
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import ZOPKKnowledgeService from zopk_knowledge_service import ZOPKKnowledgeService
db = SessionLocal() db = SessionLocal()
@ -114,13 +112,11 @@ def admin_zopk_knowledge_extract():
@bp.route('/zopk/knowledge/extract/<int:news_id>', methods=['POST']) @bp.route('/zopk/knowledge/extract/<int:news_id>', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract_single(news_id): def admin_zopk_knowledge_extract_single(news_id):
""" """
Extract knowledge from a single article. Extract knowledge from a single article.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import ZOPKKnowledgeService from zopk_knowledge_service import ZOPKKnowledgeService
db = SessionLocal() db = SessionLocal()
@ -155,6 +151,7 @@ def admin_zopk_knowledge_extract_single(news_id):
@bp.route('/zopk/knowledge/embeddings', methods=['POST']) @bp.route('/zopk/knowledge/embeddings', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_generate_embeddings(): def admin_zopk_generate_embeddings():
""" """
Generate embeddings for chunks that don't have them. Generate embeddings for chunks that don't have them.
@ -162,9 +159,6 @@ def admin_zopk_generate_embeddings():
Request JSON: Request JSON:
- limit: int (default 100) - max chunks to process - limit: int (default 100) - max chunks to process
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import generate_chunk_embeddings from zopk_knowledge_service import generate_chunk_embeddings
db = SessionLocal() db = SessionLocal()
@ -192,6 +186,7 @@ def admin_zopk_generate_embeddings():
@bp.route('/zopk/knowledge/extract/stream', methods=['GET']) @bp.route('/zopk/knowledge/extract/stream', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract_stream(): def admin_zopk_knowledge_extract_stream():
""" """
SSE endpoint for streaming knowledge extraction progress. SSE endpoint for streaming knowledge extraction progress.
@ -199,9 +194,6 @@ def admin_zopk_knowledge_extract_stream():
Query params: Query params:
- limit: int (default 10) - max articles to process - limit: int (default 10) - max articles to process
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
limit = min(int(request.args.get('limit', 10)), 50) limit = min(int(request.args.get('limit', 10)), 50)
user_id = current_user.id user_id = current_user.id
@ -275,6 +267,7 @@ def admin_zopk_knowledge_extract_stream():
@bp.route('/zopk/knowledge/embeddings/stream', methods=['GET']) @bp.route('/zopk/knowledge/embeddings/stream', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_embeddings_stream(): def admin_zopk_embeddings_stream():
""" """
SSE endpoint for streaming embeddings generation progress. SSE endpoint for streaming embeddings generation progress.
@ -282,9 +275,6 @@ def admin_zopk_embeddings_stream():
Query params: Query params:
- limit: int (default 50) - max chunks to process - limit: int (default 50) - max chunks to process
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
limit = min(int(request.args.get('limit', 50)), 200) limit = min(int(request.args.get('limit', 50)), 200)
user_id = current_user.id user_id = current_user.id
@ -413,28 +403,22 @@ def api_zopk_knowledge_search():
@bp.route('/zopk/knowledge') @bp.route('/zopk/knowledge')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_dashboard(): def admin_zopk_knowledge_dashboard():
""" """
Dashboard for ZOPK Knowledge Base management. Dashboard for ZOPK Knowledge Base management.
Shows stats and links to chunks, facts, entities lists. Shows stats and links to chunks, facts, entities lists.
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej sekcji.', 'warning')
return redirect(url_for('index'))
return render_template('admin/zopk_knowledge_dashboard.html') return render_template('admin/zopk_knowledge_dashboard.html')
@bp.route('/zopk/knowledge/chunks') @bp.route('/zopk/knowledge/chunks')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_chunks(): def admin_zopk_knowledge_chunks():
""" """
List knowledge chunks with pagination and filtering. List knowledge chunks with pagination and filtering.
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej sekcji.', 'warning')
return redirect(url_for('index'))
from zopk_knowledge_service import list_chunks from zopk_knowledge_service import list_chunks
# Get query params # Get query params
@ -478,14 +462,11 @@ def admin_zopk_knowledge_chunks():
@bp.route('/zopk/knowledge/facts') @bp.route('/zopk/knowledge/facts')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_facts(): def admin_zopk_knowledge_facts():
""" """
List knowledge facts with pagination and filtering. List knowledge facts with pagination and filtering.
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej sekcji.', 'warning')
return redirect(url_for('index'))
from zopk_knowledge_service import list_facts from zopk_knowledge_service import list_facts
# Get query params # Get query params
@ -528,14 +509,11 @@ def admin_zopk_knowledge_facts():
@bp.route('/zopk/knowledge/entities') @bp.route('/zopk/knowledge/entities')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_entities(): def admin_zopk_knowledge_entities():
""" """
List knowledge entities with pagination and filtering. List knowledge entities with pagination and filtering.
""" """
if not current_user.is_admin:
flash('Brak uprawnień do tej sekcji.', 'warning')
return redirect(url_for('index'))
from zopk_knowledge_service import list_entities from zopk_knowledge_service import list_entities
# Get query params # Get query params
@ -578,11 +556,9 @@ def admin_zopk_knowledge_entities():
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>') @bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_detail(chunk_id): def api_zopk_chunk_detail(chunk_id):
"""Get detailed information about a chunk.""" """Get detailed information about a chunk."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import get_chunk_detail from zopk_knowledge_service import get_chunk_detail
db = SessionLocal() db = SessionLocal()
@ -598,11 +574,9 @@ def api_zopk_chunk_detail(chunk_id):
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>/verify', methods=['POST']) @bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>/verify', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_verify(chunk_id): def api_zopk_chunk_verify(chunk_id):
"""Toggle chunk verification status.""" """Toggle chunk verification status."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import update_chunk_verification from zopk_knowledge_service import update_chunk_verification
db = SessionLocal() db = SessionLocal()
@ -624,11 +598,9 @@ def api_zopk_chunk_verify(chunk_id):
@bp.route('/zopk-api/knowledge/facts/<int:fact_id>/verify', methods=['POST']) @bp.route('/zopk-api/knowledge/facts/<int:fact_id>/verify', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_verify(fact_id): def api_zopk_fact_verify(fact_id):
"""Toggle fact verification status.""" """Toggle fact verification status."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import update_fact_verification from zopk_knowledge_service import update_fact_verification
db = SessionLocal() db = SessionLocal()
@ -650,11 +622,9 @@ def api_zopk_fact_verify(fact_id):
@bp.route('/zopk-api/knowledge/entities/<int:entity_id>/verify', methods=['POST']) @bp.route('/zopk-api/knowledge/entities/<int:entity_id>/verify', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_entity_verify(entity_id): def api_zopk_entity_verify(entity_id):
"""Toggle entity verification status.""" """Toggle entity verification status."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import update_entity_verification from zopk_knowledge_service import update_entity_verification
db = SessionLocal() db = SessionLocal()
@ -676,11 +646,9 @@ def api_zopk_entity_verify(entity_id):
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>', methods=['DELETE']) @bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>', methods=['DELETE'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_delete(chunk_id): def api_zopk_chunk_delete(chunk_id):
"""Delete a chunk and its associated data.""" """Delete a chunk and its associated data."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import delete_chunk from zopk_knowledge_service import delete_chunk
db = SessionLocal() db = SessionLocal()
@ -699,12 +667,9 @@ def api_zopk_chunk_delete(chunk_id):
@bp.route('/zopk/knowledge/duplicates') @bp.route('/zopk/knowledge/duplicates')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_duplicates(): def admin_zopk_knowledge_duplicates():
"""Admin page for managing duplicate entities.""" """Admin page for managing duplicate entities."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
from zopk_knowledge_service import find_duplicate_entities from zopk_knowledge_service import find_duplicate_entities
db = SessionLocal() db = SessionLocal()
@ -737,11 +702,9 @@ def admin_zopk_knowledge_duplicates():
@bp.route('/zopk-api/knowledge/duplicates/preview', methods=['POST']) @bp.route('/zopk-api/knowledge/duplicates/preview', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_duplicates_preview(): def api_zopk_duplicates_preview():
"""Preview merge operation between two entities.""" """Preview merge operation between two entities."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import get_entity_merge_preview from zopk_knowledge_service import get_entity_merge_preview
db = SessionLocal() db = SessionLocal()
@ -764,11 +727,9 @@ def api_zopk_duplicates_preview():
@bp.route('/zopk-api/knowledge/duplicates/merge', methods=['POST']) @bp.route('/zopk-api/knowledge/duplicates/merge', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_duplicates_merge(): def api_zopk_duplicates_merge():
"""Merge two entities - keep primary, delete duplicate.""" """Merge two entities - keep primary, delete duplicate."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import merge_entities from zopk_knowledge_service import merge_entities
db = SessionLocal() db = SessionLocal()
@ -789,17 +750,15 @@ def api_zopk_duplicates_merge():
@bp.route('/zopk/knowledge/graph') @bp.route('/zopk/knowledge/graph')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_graph(): def admin_zopk_knowledge_graph():
"""Admin page for entity relations graph visualization.""" """Admin page for entity relations graph visualization."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
return render_template('admin/zopk_knowledge_graph.html') return render_template('admin/zopk_knowledge_graph.html')
@bp.route('/zopk-api/knowledge/graph/data') @bp.route('/zopk-api/knowledge/graph/data')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_knowledge_graph_data(): def api_zopk_knowledge_graph_data():
"""Get graph data for entity co-occurrence visualization. """Get graph data for entity co-occurrence visualization.
@ -808,9 +767,6 @@ def api_zopk_knowledge_graph_data():
""" """
global _graph_cache global _graph_cache
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Build cache key from parameters # Build cache key from parameters
entity_type = request.args.get('entity_type', '') entity_type = request.args.get('entity_type', '')
min_cooccurrence = int(request.args.get('min_cooccurrence', 3)) min_cooccurrence = int(request.args.get('min_cooccurrence', 3))
@ -923,21 +879,17 @@ def api_zopk_knowledge_graph_data():
@bp.route('/zopk/knowledge/fact-duplicates') @bp.route('/zopk/knowledge/fact-duplicates')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_fact_duplicates(): def admin_zopk_fact_duplicates():
"""Panel deduplikacji faktów.""" """Panel deduplikacji faktów."""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('dashboard'))
return render_template('admin/zopk_fact_duplicates.html') return render_template('admin/zopk_fact_duplicates.html')
@bp.route('/zopk-api/knowledge/fact-duplicates') @bp.route('/zopk-api/knowledge/fact-duplicates')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_duplicates(): def api_zopk_fact_duplicates():
"""API - lista duplikatów faktów.""" """API - lista duplikatów faktów."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import find_duplicate_facts from zopk_knowledge_service import find_duplicate_facts
db = SessionLocal() db = SessionLocal()
try: try:
@ -955,11 +907,9 @@ def api_zopk_fact_duplicates():
@bp.route('/zopk-api/knowledge/fact-duplicates/merge', methods=['POST']) @bp.route('/zopk-api/knowledge/fact-duplicates/merge', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_merge(): def api_zopk_fact_merge():
"""API - merge duplikatów faktów.""" """API - merge duplikatów faktów."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import merge_facts from zopk_knowledge_service import merge_facts
db = SessionLocal() db = SessionLocal()
try: try:
@ -978,11 +928,9 @@ def api_zopk_fact_merge():
@bp.route('/zopk-api/knowledge/auto-verify/entities', methods=['POST']) @bp.route('/zopk-api/knowledge/auto-verify/entities', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_entities(): def api_zopk_auto_verify_entities():
"""Auto-weryfikacja encji z wysoką liczbą wzmianek.""" """Auto-weryfikacja encji z wysoką liczbą wzmianek."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import auto_verify_top_entities from zopk_knowledge_service import auto_verify_top_entities
db = SessionLocal() db = SessionLocal()
try: try:
@ -1000,11 +948,9 @@ def api_zopk_auto_verify_entities():
@bp.route('/zopk-api/knowledge/auto-verify/facts', methods=['POST']) @bp.route('/zopk-api/knowledge/auto-verify/facts', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_facts(): def api_zopk_auto_verify_facts():
"""Auto-weryfikacja faktów z wysoką ważnością.""" """Auto-weryfikacja faktów z wysoką ważnością."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import auto_verify_top_facts from zopk_knowledge_service import auto_verify_top_facts
db = SessionLocal() db = SessionLocal()
try: try:
@ -1022,11 +968,9 @@ def api_zopk_auto_verify_facts():
@bp.route('/zopk-api/knowledge/auto-verify/similar', methods=['POST']) @bp.route('/zopk-api/knowledge/auto-verify/similar', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_similar(): def api_zopk_auto_verify_similar():
"""Auto-weryfikacja faktów podobnych do już zweryfikowanych (uczenie się).""" """Auto-weryfikacja faktów podobnych do już zweryfikowanych (uczenie się)."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import auto_verify_similar_to_verified from zopk_knowledge_service import auto_verify_similar_to_verified
db = SessionLocal() db = SessionLocal()
try: try:
@ -1044,11 +988,9 @@ def api_zopk_auto_verify_similar():
@bp.route('/zopk-api/knowledge/suggest-similar-facts') @bp.route('/zopk-api/knowledge/suggest-similar-facts')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_suggest_similar_facts(): def api_zopk_suggest_similar_facts():
"""Pobierz sugestie faktów podobnych do zweryfikowanych (bez auto-weryfikacji).""" """Pobierz sugestie faktów podobnych do zweryfikowanych (bez auto-weryfikacji)."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import find_similar_to_verified_facts from zopk_knowledge_service import find_similar_to_verified_facts
db = SessionLocal() db = SessionLocal()
try: try:
@ -1069,11 +1011,9 @@ def api_zopk_suggest_similar_facts():
@bp.route('/zopk-api/knowledge/dashboard-stats') @bp.route('/zopk-api/knowledge/dashboard-stats')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_dashboard_stats(): def api_zopk_dashboard_stats():
"""API - statystyki dashboardu.""" """API - statystyki dashboardu."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import get_knowledge_dashboard_stats from zopk_knowledge_service import get_knowledge_dashboard_stats
db = SessionLocal() db = SessionLocal()
try: try:

View File

@ -18,10 +18,12 @@ from sqlalchemy.sql import nullslast
from database import ( from database import (
SessionLocal, SessionLocal,
SystemRole,
ZOPKProject, ZOPKProject,
ZOPKNews, ZOPKNews,
ZOPKNewsFetchJob ZOPKNewsFetchJob
) )
from utils.decorators import role_required
from . import bp from . import bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,11 +31,9 @@ logger = logging.getLogger(__name__)
@bp.route('/zopk/news') @bp.route('/zopk/news')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news(): def admin_zopk_news():
"""Admin news management for ZOPK""" """Admin news management for ZOPK"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal() db = SessionLocal()
try: try:
@ -90,10 +90,9 @@ def admin_zopk_news():
@bp.route('/zopk/news/<int:news_id>/approve', methods=['POST']) @bp.route('/zopk/news/<int:news_id>/approve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_approve(news_id): def admin_zopk_news_approve(news_id):
"""Approve a ZOPK news item""" """Approve a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -119,10 +118,9 @@ def admin_zopk_news_approve(news_id):
@bp.route('/zopk/news/<int:news_id>/reject', methods=['POST']) @bp.route('/zopk/news/<int:news_id>/reject', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_reject(news_id): def admin_zopk_news_reject(news_id):
"""Reject a ZOPK news item""" """Reject a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -152,10 +150,9 @@ def admin_zopk_news_reject(news_id):
@bp.route('/zopk/news/add', methods=['POST']) @bp.route('/zopk/news/add', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_add(): def admin_zopk_news_add():
"""Manually add a ZOPK news item""" """Manually add a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -233,10 +230,9 @@ def admin_zopk_news_add():
@bp.route('/zopk/news/reject-old', methods=['POST']) @bp.route('/zopk/news/reject-old', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reject_old_news(): def admin_zopk_reject_old_news():
"""Reject all news from before a certain year (ZOPK didn't exist then)""" """Reject all news from before a certain year (ZOPK didn't exist then)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -278,10 +274,9 @@ def admin_zopk_reject_old_news():
@bp.route('/zopk/news/star-counts') @bp.route('/zopk/news/star-counts')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_star_counts(): def admin_zopk_news_star_counts():
"""Get counts of pending news items grouped by star rating""" """Get counts of pending news items grouped by star rating"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -318,10 +313,9 @@ def admin_zopk_news_star_counts():
@bp.route('/zopk/news/reject-by-stars', methods=['POST']) @bp.route('/zopk/news/reject-by-stars', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reject_by_stars(): def admin_zopk_reject_by_stars():
"""Reject all pending news items with specified star ratings""" """Reject all pending news items with specified star ratings"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -383,10 +377,9 @@ def admin_zopk_reject_by_stars():
@bp.route('/zopk/news/evaluate-ai', methods=['POST']) @bp.route('/zopk/news/evaluate-ai', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_evaluate_ai(): def admin_zopk_evaluate_ai():
"""Evaluate pending news for ZOPK relevance using Gemini AI""" """Evaluate pending news for ZOPK relevance using Gemini AI"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_news_service import evaluate_pending_news from zopk_news_service import evaluate_pending_news
@ -418,10 +411,9 @@ def admin_zopk_evaluate_ai():
@bp.route('/zopk/news/reevaluate-scores', methods=['POST']) @bp.route('/zopk/news/reevaluate-scores', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reevaluate_scores(): def admin_zopk_reevaluate_scores():
"""Re-evaluate news items that have ai_relevant but no ai_relevance_score (1-5 stars)""" """Re-evaluate news items that have ai_relevant but no ai_relevance_score (1-5 stars)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_news_service import reevaluate_news_without_score from zopk_news_service import reevaluate_news_without_score
@ -453,6 +445,7 @@ def admin_zopk_reevaluate_scores():
@bp.route('/zopk/news/reevaluate-low-scores', methods=['POST']) @bp.route('/zopk/news/reevaluate-low-scores', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reevaluate_low_scores(): def admin_zopk_reevaluate_low_scores():
""" """
Re-evaluate news with low AI scores (1-2) that contain key ZOPK topics. Re-evaluate news with low AI scores (1-2) that contain key ZOPK topics.
@ -461,9 +454,6 @@ def admin_zopk_reevaluate_low_scores():
Old articles scored low before these topics were recognized will be re-evaluated Old articles scored low before these topics were recognized will be re-evaluated
and potentially upgraded. and potentially upgraded.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_news_service import reevaluate_low_score_news from zopk_news_service import reevaluate_low_score_news
db = SessionLocal() db = SessionLocal()
@ -496,6 +486,7 @@ def admin_zopk_reevaluate_low_scores():
@bp.route('/zopk-api/search-news', methods=['POST']) @bp.route('/zopk-api/search-news', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_search_news(): def api_zopk_search_news():
""" """
Search for ZOPK news using multiple sources with cross-verification. Search for ZOPK news using multiple sources with cross-verification.
@ -509,9 +500,6 @@ def api_zopk_search_news():
- 1 source pending (manual review) - 1 source pending (manual review)
- 3+ sources auto_approved - 3+ sources auto_approved
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_news_service import ZOPKNewsService from zopk_news_service import ZOPKNewsService
db = SessionLocal() db = SessionLocal()
@ -594,6 +582,7 @@ def api_zopk_search_news():
@bp.route('/zopk/news/scrape-stats') @bp.route('/zopk/news/scrape-stats')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_stats(): def admin_zopk_scrape_stats():
""" """
Get content scraping statistics. Get content scraping statistics.
@ -606,9 +595,6 @@ def admin_zopk_scrape_stats():
- skipped: Skipped (social media, paywalls) - skipped: Skipped (social media, paywalls)
- ready_for_extraction: Scraped but not yet processed for knowledge - ready_for_extraction: Scraped but not yet processed for knowledge
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_content_scraper import get_scrape_stats from zopk_content_scraper import get_scrape_stats
db = SessionLocal() db = SessionLocal()
@ -627,6 +613,7 @@ def admin_zopk_scrape_stats():
@bp.route('/zopk/news/scrape-content', methods=['POST']) @bp.route('/zopk/news/scrape-content', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_content(): def admin_zopk_scrape_content():
""" """
Batch scrape article content from source URLs. Batch scrape article content from source URLs.
@ -642,9 +629,6 @@ def admin_zopk_scrape_content():
- errors: list of error details - errors: list of error details
- scraped_articles: list of scraped article info - scraped_articles: list of scraped article info
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_content_scraper import ZOPKContentScraper from zopk_content_scraper import ZOPKContentScraper
db = SessionLocal() db = SessionLocal()
@ -673,13 +657,11 @@ def admin_zopk_scrape_content():
@bp.route('/zopk/news/<int:news_id>/scrape', methods=['POST']) @bp.route('/zopk/news/<int:news_id>/scrape', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_single(news_id): def admin_zopk_scrape_single(news_id):
""" """
Scrape content for a single article. Scrape content for a single article.
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_content_scraper import ZOPKContentScraper from zopk_content_scraper import ZOPKContentScraper
db = SessionLocal() db = SessionLocal()
@ -711,6 +693,7 @@ def admin_zopk_scrape_single(news_id):
@bp.route('/zopk/news/scrape-content/stream', methods=['GET']) @bp.route('/zopk/news/scrape-content/stream', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_scrape_stream(): def admin_zopk_news_scrape_stream():
""" """
Stream scraping progress using Server-Sent Events. Stream scraping progress using Server-Sent Events.
@ -719,9 +702,6 @@ def admin_zopk_news_scrape_stream():
- limit: int (default 50) - limit: int (default 50)
- force: bool (default false) - force: bool (default false)
""" """
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_content_scraper import ZOPKContentScraper, MAX_RETRY_ATTEMPTS from zopk_content_scraper import ZOPKContentScraper, MAX_RETRY_ATTEMPTS
limit = request.args.get('limit', 50, type=int) limit = request.args.get('limit', 50, type=int)

View File

@ -13,8 +13,10 @@ from flask_login import current_user, login_required
from database import ( from database import (
SessionLocal, SessionLocal,
SystemRole,
ZOPKMilestone ZOPKMilestone
) )
from utils.decorators import role_required
from . import bp from . import bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,11 +24,9 @@ logger = logging.getLogger(__name__)
@bp.route('/zopk/timeline') @bp.route('/zopk/timeline')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_timeline(): def admin_zopk_timeline():
"""Panel Timeline ZOPK.""" """Panel Timeline ZOPK."""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('dashboard'))
return render_template('admin/zopk_timeline.html') return render_template('admin/zopk_timeline.html')
@ -58,10 +58,9 @@ def api_zopk_milestones():
@bp.route('/zopk-api/milestones', methods=['POST']) @bp.route('/zopk-api/milestones', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_create(): def api_zopk_milestone_create():
"""API - utworzenie kamienia milowego.""" """API - utworzenie kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -88,10 +87,9 @@ def api_zopk_milestone_create():
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['PUT']) @bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['PUT'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_update(milestone_id): def api_zopk_milestone_update(milestone_id):
"""API - aktualizacja kamienia milowego.""" """API - aktualizacja kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -126,10 +124,9 @@ def api_zopk_milestone_update(milestone_id):
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['DELETE']) @bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['DELETE'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_delete(milestone_id): def api_zopk_milestone_delete(milestone_id):
"""API - usunięcie kamienia milowego.""" """API - usunięcie kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal() db = SessionLocal()
try: try:
@ -149,10 +146,9 @@ def api_zopk_milestone_delete(milestone_id):
@bp.route('/zopk-api/timeline/suggestions') @bp.route('/zopk-api/timeline/suggestions')
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_timeline_suggestions(): def api_zopk_timeline_suggestions():
"""API - sugestie kamieni milowych z bazy wiedzy.""" """API - sugestie kamieni milowych z bazy wiedzy."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import get_timeline_suggestions from zopk_knowledge_service import get_timeline_suggestions
@ -177,10 +173,9 @@ def api_zopk_timeline_suggestions():
@bp.route('/zopk-api/timeline/suggestions/approve', methods=['POST']) @bp.route('/zopk-api/timeline/suggestions/approve', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN)
def api_zopk_timeline_suggestion_approve(): def api_zopk_timeline_suggestion_approve():
"""API - zatwierdzenie sugestii i utworzenie kamienia milowego.""" """API - zatwierdzenie sugestii i utworzenie kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import create_milestone_from_suggestion from zopk_knowledge_service import create_milestone_from_suggestion

View File

@ -511,9 +511,9 @@ def api_enrich_company_ai(company_id):
'error': 'Firma nie znaleziona' 'error': 'Firma nie znaleziona'
}), 404 }), 404
# Check permissions: admin or company owner # Check permissions: user with company edit rights
logger.info(f"Permission check: user={current_user.email}, is_admin={current_user.is_admin}, user_company_id={current_user.company_id}, target_company_id={company.id}") logger.info(f"Permission check: user={current_user.email}, is_admin={current_user.is_admin}, user_company_id={current_user.company_id}, target_company_id={company.id}")
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnien. Tylko administrator lub wlasciciel firmy moze wzbogacac dane.' 'error': 'Brak uprawnien. Tylko administrator lub wlasciciel firmy moze wzbogacac dane.'
@ -755,8 +755,8 @@ def api_get_proposals(company_id):
if not company: if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions # Check permissions - user with company edit rights
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposals = db.query(AiEnrichmentProposal).filter_by( proposals = db.query(AiEnrichmentProposal).filter_by(
@ -798,8 +798,8 @@ def api_approve_proposal(company_id, proposal_id):
if not company: if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions - only admin or company owner # Check permissions - user with company edit rights
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposal = db.query(AiEnrichmentProposal).filter_by( proposal = db.query(AiEnrichmentProposal).filter_by(
@ -904,8 +904,8 @@ def api_reject_proposal(company_id, proposal_id):
if not company: if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions # Check permissions - user with company edit rights
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposal = db.query(AiEnrichmentProposal).filter_by( proposal = db.query(AiEnrichmentProposal).filter_by(
@ -972,7 +972,7 @@ def test_sanitization():
Admin API: Test sensitive data detection without saving. Admin API: Test sensitive data detection without saving.
Allows admins to verify what data would be sanitized. Allows admins to verify what data would be sanitized.
""" """
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Admin access required'}), 403 return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:

View File

@ -283,14 +283,12 @@ def api_gbp_audit_trigger():
'error': 'Firma nie znaleziona lub nieaktywna.' 'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404 }), 404
# Check access: admin can audit any company, member only their own # Check access: users with company edit rights can audit
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
# Check if user is associated with this company return jsonify({
if current_user.company_id != company.id: 'success': False,
return jsonify({ 'error': 'Brak uprawnień. Możesz audytować tylko własną firmę.'
'success': False, }), 403
'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})") logger.info(f"GBP audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})")

View File

@ -233,8 +233,8 @@ def api_edit_recommendation(rec_id):
'error': 'Rekomendacja nie znaleziona' 'error': 'Rekomendacja nie znaleziona'
}), 404 }), 404
# Check authorization - user must be the owner OR admin # Check authorization - user must be the owner OR have admin panel access
if recommendation.user_id != current_user.id and not current_user.is_admin: if recommendation.user_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnień do edycji tej rekomendacji' 'error': 'Brak uprawnień do edycji tej rekomendacji'
@ -313,8 +313,8 @@ def api_delete_recommendation(rec_id):
'error': 'Rekomendacja nie znaleziona' 'error': 'Rekomendacja nie znaleziona'
}), 404 }), 404
# Check authorization - user must be the owner OR admin # Check authorization - user must be the owner OR have admin panel access
if recommendation.user_id != current_user.id and not current_user.is_admin: if recommendation.user_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnień do usunięcia tej rekomendacji' 'error': 'Brak uprawnień do usunięcia tej rekomendacji'

View File

@ -336,8 +336,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
""" """
# Admin-only check # Check admin panel access
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnień. Tylko administrator może uruchamiać audyty SEO.' 'error': 'Brak uprawnień. Tylko administrator może uruchamiać audyty SEO.'

View File

@ -88,13 +88,12 @@ def api_social_audit_trigger():
'error': 'Firma nie znaleziona lub nieaktywna.' 'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404 }), 404
# Access control - admin can audit all, users only their company # Access control - users with admin panel access or company edit rights can audit
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
if current_user.company_id != company.id: return jsonify({
return jsonify({ 'success': False,
'success': False, 'error': 'Brak uprawnień do audytu social media tej firmy.'
'error': 'Brak uprawnień do audytu social media tej firmy.' }), 403
}), 403
logger.info(f"Social Media audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})") logger.info(f"Social Media audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})")

View File

@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 31, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #175 | 6:24 PM | 🔵 | Nordabiz audit blueprint provides user-facing dashboards for SEO, GBP, social media, and IT audits | ~542 |
</claude-mem-context>

View File

@ -66,11 +66,10 @@ def seo_audit_dashboard(slug):
flash('Firma nie została znaleziona.', 'error') flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own # Access control: users with company edit rights can view
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
if current_user.company_id != company.id: flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error') return redirect(url_for('dashboard'))
return redirect(url_for('dashboard'))
# Get latest SEO analysis for this company # Get latest SEO analysis for this company
analysis = db.query(CompanyWebsiteAnalysis).filter( analysis = db.query(CompanyWebsiteAnalysis).filter(
@ -90,8 +89,8 @@ def seo_audit_dashboard(slug):
'url': analysis.website_url 'url': analysis.website_url
} }
# Determine if user can run audit (admin or company owner) # Determine if user can run audit (user with company edit rights)
can_audit = current_user.is_admin or current_user.company_id == company.id can_audit = current_user.can_edit_company(company.id)
logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}") logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}")
@ -139,11 +138,10 @@ def social_audit_dashboard(slug):
flash('Firma nie została znaleziona.', 'error') flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Access control - admin can view all, users only their company # Access control - users with company edit rights can view
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
if current_user.company_id != company.id: flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error') return redirect(url_for('dashboard'))
return redirect(url_for('dashboard'))
# Get social media profiles for this company # Get social media profiles for this company
social_profiles = db.query(CompanySocialMedia).filter( social_profiles = db.query(CompanySocialMedia).filter(
@ -179,8 +177,8 @@ def social_audit_dashboard(slug):
'score': score 'score': score
} }
# Determine if user can run audit (admin or company owner) # Determine if user can run audit (user with company edit rights)
can_audit = current_user.is_admin or current_user.company_id == company.id can_audit = current_user.can_edit_company(company.id)
logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}") logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}")
@ -233,11 +231,10 @@ def gbp_audit_dashboard(slug):
flash('Firma nie została znaleziona.', 'error') flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own # Access control: users with company edit rights can view
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
if current_user.company_id != company.id: flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error') return redirect(url_for('dashboard'))
return redirect(url_for('dashboard'))
# Get latest audit for this company # Get latest audit for this company
audit = gbp_get_company_audit(db, company.id) audit = gbp_get_company_audit(db, company.id)
@ -245,8 +242,8 @@ def gbp_audit_dashboard(slug):
# If no audit exists, we still render the page (template handles this) # If no audit exists, we still render the page (template handles this)
# The user can trigger an audit from the dashboard # The user can trigger an audit from the dashboard
# Determine if user can run audit (admin or company owner) # Determine if user can run audit (user with company edit rights)
can_audit = current_user.is_admin or current_user.company_id == company.id can_audit = current_user.can_edit_company(company.id)
logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}") logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}")
@ -297,11 +294,10 @@ def it_audit_dashboard(slug):
flash('Firma nie została znaleziona.', 'error') flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own # Access control: users with company edit rights can view
if not current_user.is_admin: if not current_user.can_edit_company(company.id):
if current_user.company_id != company.id: flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error') return redirect(url_for('dashboard'))
return redirect(url_for('dashboard'))
# Get latest IT audit for this company # Get latest IT audit for this company
audit = db.query(ITAudit).filter( audit = db.query(ITAudit).filter(
@ -356,8 +352,8 @@ def it_audit_dashboard(slug):
'recommendations': audit.recommendations 'recommendations': audit.recommendations
} }
# Determine if user can edit audit (admin or company owner) # Determine if user can edit audit (user with company edit rights)
can_edit = current_user.is_admin or current_user.company_id == company.id can_edit = current_user.can_edit_company(company.id)
logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}") logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}")

11
blueprints/chat/CLAUDE.md Normal file
View File

@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 31, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #180 | 6:25 PM | 🔵 | Nordabiz project architecture analyzed revealing 16+ Flask blueprints with modular organization | ~831 |
</claude-mem-context>

View File

@ -394,8 +394,8 @@ def chat_feedback():
@login_required @login_required
def chat_analytics(): def chat_analytics():
"""Admin dashboard for chat analytics""" """Admin dashboard for chat analytics"""
# Only admins can access # Only users with admin panel access can view chat analytics
if not current_user.is_admin: if not current_user.can_access_admin_panel():
flash('Brak uprawnień do tej strony.', 'error') flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))

View File

@ -159,7 +159,7 @@ def view(classified_id):
questions_query = db.query(ClassifiedQuestion).filter( questions_query = db.query(ClassifiedQuestion).filter(
ClassifiedQuestion.classified_id == classified.id ClassifiedQuestion.classified_id == classified.id
) )
if classified.author_id != current_user.id and not current_user.is_admin: if classified.author_id != current_user.id and not current_user.can_access_admin_panel():
questions_query = questions_query.filter(ClassifiedQuestion.is_public == True) questions_query = questions_query.filter(ClassifiedQuestion.is_public == True)
questions = questions_query.order_by(ClassifiedQuestion.created_at.asc()).all() questions = questions_query.order_by(ClassifiedQuestion.created_at.asc()).all()
@ -209,7 +209,7 @@ def close(classified_id):
@login_required @login_required
def delete(classified_id): def delete(classified_id):
"""Usuń ogłoszenie (admin only)""" """Usuń ogłoszenie (admin only)"""
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -233,7 +233,7 @@ def delete(classified_id):
@login_required @login_required
def toggle_active(classified_id): def toggle_active(classified_id):
"""Aktywuj/dezaktywuj ogłoszenie (admin only)""" """Aktywuj/dezaktywuj ogłoszenie (admin only)"""
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -323,8 +323,8 @@ def list_interests(classified_id):
if not classified: if not classified:
return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404
# Tylko autor może widzieć pełną listę # Tylko autor może widzieć pełną listę (lub admin)
if classified.author_id != current_user.id and not current_user.is_admin: if classified.author_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
interests = db.query(ClassifiedInterest).filter( interests = db.query(ClassifiedInterest).filter(
@ -469,7 +469,7 @@ def hide_question(classified_id, question_id):
return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404
# Tylko autor ogłoszenia lub admin może ukrywać # Tylko autor ogłoszenia lub admin może ukrywać
if classified.author_id != current_user.id and not current_user.is_admin: if classified.author_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
question = db.query(ClassifiedQuestion).filter( question = db.query(ClassifiedQuestion).filter(
@ -511,7 +511,7 @@ def list_questions(classified_id):
ClassifiedQuestion.classified_id == classified_id ClassifiedQuestion.classified_id == classified_id
) )
if classified.author_id != current_user.id and not current_user.is_admin: if classified.author_id != current_user.id and not current_user.can_access_admin_panel():
query = query.filter(ClassifiedQuestion.is_public == True) query = query.filter(ClassifiedQuestion.is_public == True)
questions = query.order_by(desc(ClassifiedQuestion.created_at)).all() questions = query.order_by(desc(ClassifiedQuestion.created_at)).all()

View File

@ -118,7 +118,7 @@ def detail(contact_id):
).order_by(ExternalContact.last_name).limit(5).all() ).order_by(ExternalContact.last_name).limit(5).all()
# Check if current user can edit (creator or admin) # Check if current user can edit (creator or admin)
can_edit = (current_user.is_admin or can_edit = (current_user.can_access_admin_panel() or
(contact.created_by and contact.created_by == current_user.id)) (contact.created_by and contact.created_by == current_user.id))
return render_template('contacts/detail.html', return render_template('contacts/detail.html',
@ -213,8 +213,8 @@ def edit(contact_id):
flash('Kontakt nie został znaleziony.', 'error') flash('Kontakt nie został znaleziony.', 'error')
return redirect(url_for('.contacts_list')) return redirect(url_for('.contacts_list'))
# Check permissions # Check permissions - creator or admin
if not current_user.is_admin and contact.created_by != current_user.id: if not current_user.can_access_admin_panel() and contact.created_by != current_user.id:
flash('Nie masz uprawnień do edycji tego kontaktu.', 'error') flash('Nie masz uprawnień do edycji tego kontaktu.', 'error')
return redirect(url_for('.contact_detail', contact_id=contact_id)) return redirect(url_for('.contact_detail', contact_id=contact_id))
@ -282,8 +282,8 @@ def delete(contact_id):
flash('Kontakt nie został znaleziony.', 'error') flash('Kontakt nie został znaleziony.', 'error')
return redirect(url_for('.contacts_list')) return redirect(url_for('.contacts_list'))
# Check permissions # Check permissions - creator or admin
if not current_user.is_admin and contact.created_by != current_user.id: if not current_user.can_access_admin_panel() and contact.created_by != current_user.id:
flash('Nie masz uprawnień do usunięcia tego kontaktu.', 'error') flash('Nie masz uprawnień do usunięcia tego kontaktu.', 'error')
return redirect(url_for('.contact_detail', contact_id=contact_id)) return redirect(url_for('.contact_detail', contact_id=contact_id))

View File

@ -221,8 +221,8 @@ def forum_topic(topic_id):
flash('Temat nie istnieje.', 'error') flash('Temat nie istnieje.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
# Check if topic is soft-deleted (only admins can view) # Check if topic is soft-deleted (only moderators can view)
if topic.is_deleted and not current_user.is_admin: if topic.is_deleted and not current_user.can_moderate_forum():
flash('Temat nie istnieje.', 'error') flash('Temat nie istnieje.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
@ -237,9 +237,9 @@ def forum_topic(topic_id):
if not existing_topic_read: if not existing_topic_read:
db.add(ForumTopicRead(topic_id=topic.id, user_id=current_user.id)) db.add(ForumTopicRead(topic_id=topic.id, user_id=current_user.id))
# Filter soft-deleted replies for non-admins # Filter soft-deleted replies for non-moderators
visible_replies = [r for r in topic.replies visible_replies = [r for r in topic.replies
if not r.is_deleted or current_user.is_admin] if not r.is_deleted or current_user.can_moderate_forum()]
# Record read for all visible replies # Record read for all visible replies
for reply in visible_replies: for reply in visible_replies:
@ -397,7 +397,7 @@ def forum_reply(topic_id):
@login_required @login_required
def admin_forum(): def admin_forum():
"""Admin panel for forum moderation""" """Admin panel for forum moderation"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
flash('Brak uprawnień do tej strony.', 'error') flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
@ -451,7 +451,7 @@ def admin_forum():
@login_required @login_required
def admin_forum_pin(topic_id): def admin_forum_pin(topic_id):
"""Toggle topic pin status""" """Toggle topic pin status"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -477,7 +477,7 @@ def admin_forum_pin(topic_id):
@login_required @login_required
def admin_forum_lock(topic_id): def admin_forum_lock(topic_id):
"""Toggle topic lock status""" """Toggle topic lock status"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -503,7 +503,7 @@ def admin_forum_lock(topic_id):
@login_required @login_required
def admin_forum_delete_topic(topic_id): def admin_forum_delete_topic(topic_id):
"""Delete topic and all its replies""" """Delete topic and all its replies"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -529,7 +529,7 @@ def admin_forum_delete_topic(topic_id):
@login_required @login_required
def admin_forum_delete_reply(reply_id): def admin_forum_delete_reply(reply_id):
"""Delete a reply""" """Delete a reply"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -554,8 +554,8 @@ def admin_forum_delete_reply(reply_id):
@bp.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST']) @bp.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST'])
@login_required @login_required
def admin_forum_change_status(topic_id): def admin_forum_change_status(topic_id):
"""Change topic status (admin only)""" """Change topic status (moderators only)"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -593,8 +593,8 @@ def admin_forum_change_status(topic_id):
@bp.route('/admin/forum/bulk-action', methods=['POST']) @bp.route('/admin/forum/bulk-action', methods=['POST'])
@login_required @login_required
def admin_forum_bulk_action(): def admin_forum_bulk_action():
"""Perform bulk action on multiple topics (admin only)""" """Perform bulk action on multiple topics (moderators only)"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -700,12 +700,12 @@ def edit_topic(topic_id):
if not topic: if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
# Check ownership (unless admin) # Check ownership (unless moderator)
if topic.author_id != current_user.id and not current_user.is_admin: if topic.author_id != current_user.id and not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Check time limit (unless admin) # Check time limit (unless moderator)
if not current_user.is_admin and not _can_edit_content(topic.created_at): if not current_user.can_moderate_forum() and not _can_edit_content(topic.created_at):
return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403 return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403
if topic.is_locked: if topic.is_locked:
@ -756,12 +756,12 @@ def edit_reply(reply_id):
if not reply: if not reply:
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
# Check ownership (unless admin) # Check ownership (unless moderator)
if reply.author_id != current_user.id and not current_user.is_admin: if reply.author_id != current_user.id and not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Check time limit (unless admin) # Check time limit (unless moderator)
if not current_user.is_admin and not _can_edit_content(reply.created_at): if not current_user.can_moderate_forum() and not _can_edit_content(reply.created_at):
return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403 return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403
# Check if topic is locked # Check if topic is locked
@ -809,7 +809,7 @@ def delete_own_reply(reply_id):
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404 return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
# Check ownership # Check ownership
if reply.author_id != current_user.id and not current_user.is_admin: if reply.author_id != current_user.id and not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Check if topic is locked # Check if topic is locked
@ -1094,8 +1094,8 @@ def report_content():
@bp.route('/admin/forum/topic/<int:topic_id>/admin-edit', methods=['POST']) @bp.route('/admin/forum/topic/<int:topic_id>/admin-edit', methods=['POST'])
@login_required @login_required
def admin_edit_topic(topic_id): def admin_edit_topic(topic_id):
"""Admin: Edit any topic content""" """Moderator: Edit any topic content"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -1150,8 +1150,8 @@ def admin_edit_topic(topic_id):
@bp.route('/admin/forum/reply/<int:reply_id>/admin-edit', methods=['POST']) @bp.route('/admin/forum/reply/<int:reply_id>/admin-edit', methods=['POST'])
@login_required @login_required
def admin_edit_reply(reply_id): def admin_edit_reply(reply_id):
"""Admin: Edit any reply content""" """Moderator: Edit any reply content"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -1196,8 +1196,8 @@ def admin_edit_reply(reply_id):
@bp.route('/admin/forum/reply/<int:reply_id>/solution', methods=['POST']) @bp.route('/admin/forum/reply/<int:reply_id>/solution', methods=['POST'])
@login_required @login_required
def mark_as_solution(reply_id): def mark_as_solution(reply_id):
"""Admin: Mark reply as solution""" """Moderator: Mark reply as solution"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -1251,8 +1251,8 @@ def mark_as_solution(reply_id):
@bp.route('/admin/forum/topic/<int:topic_id>/restore', methods=['POST']) @bp.route('/admin/forum/topic/<int:topic_id>/restore', methods=['POST'])
@login_required @login_required
def restore_topic(topic_id): def restore_topic(topic_id):
"""Admin: Restore soft-deleted topic""" """Moderator: Restore soft-deleted topic"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -1281,8 +1281,8 @@ def restore_topic(topic_id):
@bp.route('/admin/forum/reply/<int:reply_id>/restore', methods=['POST']) @bp.route('/admin/forum/reply/<int:reply_id>/restore', methods=['POST'])
@login_required @login_required
def restore_reply(reply_id): def restore_reply(reply_id):
"""Admin: Restore soft-deleted reply""" """Moderator: Restore soft-deleted reply"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -1311,8 +1311,8 @@ def restore_reply(reply_id):
@bp.route('/admin/forum/reports') @bp.route('/admin/forum/reports')
@login_required @login_required
def admin_forum_reports(): def admin_forum_reports():
"""Admin: View all reports""" """Moderator: View all reports"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
flash('Brak uprawnień do tej strony.', 'error') flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
@ -1348,8 +1348,8 @@ def admin_forum_reports():
@bp.route('/admin/forum/report/<int:report_id>/review', methods=['POST']) @bp.route('/admin/forum/report/<int:report_id>/review', methods=['POST'])
@login_required @login_required
def review_report(report_id): def review_report(report_id):
"""Admin: Review a report""" """Moderator: Review a report"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -1383,8 +1383,8 @@ def review_report(report_id):
@bp.route('/admin/forum/topic/<int:topic_id>/history') @bp.route('/admin/forum/topic/<int:topic_id>/history')
@login_required @login_required
def topic_edit_history(topic_id): def topic_edit_history(topic_id):
"""Admin: View topic edit history""" """Moderator: View topic edit history"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -1412,8 +1412,8 @@ def topic_edit_history(topic_id):
@bp.route('/admin/forum/reply/<int:reply_id>/history') @bp.route('/admin/forum/reply/<int:reply_id>/history')
@login_required @login_required
def reply_edit_history(reply_id): def reply_edit_history(reply_id):
"""Admin: View reply edit history""" """Moderator: View reply edit history"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal() db = SessionLocal()
@ -1441,8 +1441,8 @@ def reply_edit_history(reply_id):
@bp.route('/admin/forum/deleted') @bp.route('/admin/forum/deleted')
@login_required @login_required
def admin_deleted_content(): def admin_deleted_content():
"""Admin: View soft-deleted topics and replies""" """Moderator: View soft-deleted topics and replies"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
flash('Brak uprawnień do tej strony.', 'error') flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
@ -1553,7 +1553,7 @@ def user_forum_stats(user_id):
@login_required @login_required
def admin_forum_analytics(): def admin_forum_analytics():
"""Forum analytics dashboard with stats, charts, and rankings""" """Forum analytics dashboard with stats, charts, and rankings"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
flash('Brak uprawnien do tej strony.', 'error') flash('Brak uprawnien do tej strony.', 'error')
return redirect(url_for('.forum_index')) return redirect(url_for('.forum_index'))
@ -1787,7 +1787,7 @@ def admin_forum_analytics():
@login_required @login_required
def admin_forum_export_activity(): def admin_forum_export_activity():
"""Export forum activity to CSV""" """Export forum activity to CSV"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
from flask import Response from flask import Response
@ -1880,7 +1880,7 @@ def admin_forum_export_activity():
@login_required @login_required
def admin_move_topic(topic_id): def admin_move_topic(topic_id):
"""Move topic to different category""" """Move topic to different category"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -1914,7 +1914,7 @@ def admin_move_topic(topic_id):
@login_required @login_required
def admin_merge_topics(): def admin_merge_topics():
"""Merge multiple topics into one""" """Merge multiple topics into one"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
data = request.get_json() or {} data = request.get_json() or {}
@ -1986,8 +1986,8 @@ def admin_merge_topics():
@bp.route('/admin/forum/search') @bp.route('/admin/forum/search')
@login_required @login_required
def admin_forum_search(): def admin_forum_search():
"""Search all forum content (including deleted) - admin only""" """Search all forum content (including deleted) - moderators only"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
query = request.args.get('q', '').strip() query = request.args.get('q', '').strip()
@ -2076,8 +2076,8 @@ def admin_forum_search():
@bp.route('/admin/forum/user/<int:user_id>/activity') @bp.route('/admin/forum/user/<int:user_id>/activity')
@login_required @login_required
def admin_user_forum_activity(user_id): def admin_user_forum_activity(user_id):
"""Get detailed forum activity for a specific user - admin only""" """Get detailed forum activity for a specific user - moderators only"""
if not current_user.is_admin: if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403 return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
from sqlalchemy import func from sqlalchemy import func

View File

@ -71,7 +71,7 @@ def it_audit_form():
# If no company_id provided, use current user's company # If no company_id provided, use current user's company
if current_user.company_id: if current_user.company_id:
company_id = current_user.company_id company_id = current_user.company_id
elif current_user.is_admin: elif current_user.can_access_admin_panel():
# Admin without specific company_id should redirect to admin dashboard # Admin without specific company_id should redirect to admin dashboard
flash('Wybierz firmę do przeprowadzenia audytu IT.', 'info') flash('Wybierz firmę do przeprowadzenia audytu IT.', 'info')
return redirect(url_for('admin_it_audit')) return redirect(url_for('admin_it_audit'))
@ -89,8 +89,8 @@ def it_audit_form():
flash('Firma nie została znaleziona.', 'error') flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Access control: admin can access any company, users only their own # Access control: users with company edit rights can access
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
flash('Nie masz uprawnień do edycji audytu IT tej firmy.', 'error') flash('Nie masz uprawnień do edycji audytu IT tej firmy.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
@ -193,8 +193,8 @@ def it_audit_save():
'error': 'Firma nie znaleziona lub nieaktywna.' 'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404 }), 404
# Access control: admin can save for any company, users only their own # Access control: users with company edit rights can save
if not current_user.is_admin and current_user.company_id != company.id: if not current_user.can_edit_company(company.id):
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Nie masz uprawnień do edycji audytu IT tej firmy.' 'error': 'Nie masz uprawnień do edycji audytu IT tej firmy.'

View File

@ -43,8 +43,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
""" """
# Only admins can view collaboration matches # Only users with admin panel access can view collaboration matches
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnień. Tylko administrator może przeglądać dopasowania.' 'error': 'Brak uprawnień. Tylko administrator może przeglądać dopasowania.'
@ -138,8 +138,8 @@ def api_it_audit_history(company_id):
""" """
from it_audit_service import get_company_audit_history from it_audit_service import get_company_audit_history
# Access control: users can only view their own company's history # Access control: users with company edit rights can view history
if not current_user.is_admin and current_user.company_id != company_id: if not current_user.can_edit_company(company_id):
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Brak uprawnień do przeglądania historii audytów tej firmy.' 'error': 'Brak uprawnień do przeglądania historii audytów tej firmy.'
@ -210,7 +210,7 @@ def api_it_audit_export():
Returns: Returns:
CSV file with IT audit data CSV file with IT audit data
""" """
if not current_user.is_admin: if not current_user.can_access_admin_panel():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Tylko administrator może eksportować dane audytów.' 'error': 'Tylko administrator może eksportować dane audytów.'

View File

@ -186,10 +186,10 @@ def company_detail(company_id):
company_id=company_id company_id=company_id
).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all() ).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all()
# Check if current user can enrich company data (admin or company owner) # Check if current user can enrich company data (user with company edit rights)
can_enrich = False can_enrich = False
if current_user.is_authenticated: if current_user.is_authenticated:
can_enrich = current_user.is_admin or (current_user.company_id == company.id) can_enrich = current_user.can_edit_company(company.id)
return render_template('company_detail.html', return render_template('company_detail.html',
company=company, company=company,

View File

@ -0,0 +1,32 @@
-- Migration: Sync user roles with is_admin flag
-- Date: 2026-02-01
-- Description: Ensures all users have proper role field based on is_admin and company membership
-- Part of: Role-based access control migration from is_admin to SystemRole
-- 1. Set ADMIN role for users with is_admin=true
UPDATE users
SET role = 'ADMIN'
WHERE is_admin = true AND (role IS NULL OR role != 'ADMIN');
-- 2. Set MEMBER role for non-admin users who have is_norda_member=true but no company
UPDATE users
SET role = 'MEMBER'
WHERE is_admin = false
AND is_norda_member = true
AND company_id IS NULL
AND (role IS NULL OR role = 'UNAFFILIATED');
-- 3. Set EMPLOYEE role for non-admin users who have a company assigned
UPDATE users
SET role = 'EMPLOYEE'
WHERE is_admin = false
AND company_id IS NOT NULL
AND (role IS NULL OR role = 'UNAFFILIATED');
-- 4. Set UNAFFILIATED for remaining users without role
UPDATE users
SET role = 'UNAFFILIATED'
WHERE role IS NULL;
-- 5. Verify: Show role distribution after migration
-- SELECT role, COUNT(*) as count FROM users GROUP BY role ORDER BY role;

View File

@ -1339,7 +1339,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg> </svg>
</a> </a>
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="btn-icon edit" title="{{ 'Edytuj audyt' if has_audit else 'Utwórz audyt' }}"> <a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="btn-icon edit" title="{{ 'Edytuj audyt' if has_audit else 'Utwórz audyt' }}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if has_audit %} {% if has_audit %}

View File

@ -558,7 +558,7 @@
</svg> </svg>
API API
</a> </a>
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<button class="btn btn-primary btn-sm" onclick="runBatchAudit()" id="batchAuditBtn"> <button class="btn btn-primary btn-sm" onclick="runBatchAudit()" id="batchAuditBtn">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
@ -758,7 +758,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg> </svg>
</a> </a>
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<button class="btn-icon audit" onclick="runSingleAudit('{{ company.slug }}')" title="Uruchom audyt SEO"> <button class="btn-icon audit" onclick="runSingleAudit('{{ company.slug }}')" title="Uruchom audyt SEO">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>

View File

@ -1215,7 +1215,7 @@
</div> </div>
</header> </header>
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<!-- Admin Bar --> <!-- Admin Bar -->
<div class="admin-bar"> <div class="admin-bar">
<div class="admin-bar-inner"> <div class="admin-bar-inner">

View File

@ -375,7 +375,7 @@
</div> </div>
{% endif %} {% endif %}
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a> <a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
{% endif %} {% endif %}
</div> </div>

View File

@ -9,7 +9,7 @@
/* Reset dla pełnoekranowego chatu jak ChatGPT/Claude */ /* Reset dla pełnoekranowego chatu jak ChatGPT/Claude */
:root { :root {
/* Wysokość nagłówka: 73px navbar + 36px admin bar (jeśli admin) */ /* Wysokość nagłówka: 73px navbar + 36px admin bar (jeśli admin) */
--header-height: {% if current_user.is_authenticated and current_user.is_admin %}109px{% else %}73px{% endif %}; --header-height: {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}109px{% else %}73px{% endif %};
} }
html, body { html, body {

View File

@ -628,7 +628,7 @@
{% if classified.author_id == current_user.id %} {% if classified.author_id == current_user.id %}
<button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button> <button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button>
{% endif %} {% endif %}
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<div class="admin-actions"> <div class="admin-actions">
<button type="button" class="admin-btn admin-btn-toggle {% if not classified.is_active %}inactive{% endif %}" onclick="toggleActive()" title="{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}"> <button type="button" class="admin-btn admin-btn-toggle {% if not classified.is_active %}inactive{% endif %}" onclick="toggleActive()" title="{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">

View File

@ -740,7 +740,7 @@
{# GBP Audit link - visible to admins (all profiles) or regular users (own company only) #} {# GBP Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %} {% if current_user.can_edit_company(company.id) %}
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
@ -752,7 +752,7 @@
{# SEO Audit link - visible to admins (all profiles) or regular users (own company only) #} {# SEO Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %} {% if current_user.can_edit_company(company.id) %}
<a href="{{ url_for('seo_audit_dashboard', slug=company.slug) }}" class="contact-bar-item seo-audit" title="Audyt SEO strony WWW"> <a href="{{ url_for('seo_audit_dashboard', slug=company.slug) }}" class="contact-bar-item seo-audit" title="Audyt SEO strony WWW">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
@ -764,7 +764,7 @@
{# Social Media Audit link - visible to admins (all profiles) or regular users (own company only) #} {# Social Media Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %} {% if current_user.can_edit_company(company.id) %}
<a href="{{ url_for('social_audit_dashboard', slug=company.slug) }}" class="contact-bar-item social-audit" title="Audyt Social Media"> <a href="{{ url_for('social_audit_dashboard', slug=company.slug) }}" class="contact-bar-item social-audit" title="Audyt Social Media">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
@ -776,7 +776,7 @@
{# IT Infrastructure Audit link - visible to admins (all profiles) or regular users (own company only) #} {# IT Infrastructure Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %} {% if current_user.can_edit_company(company.id) %}
<a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="contact-bar-item it-audit" title="Audyt Infrastruktury IT"> <a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="contact-bar-item it-audit" title="Audyt Infrastruktury IT">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/> <path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>

View File

@ -388,7 +388,7 @@
</h2> </h2>
<p style="color: var(--text-secondary); margin: 8px 0 0 0;"> <p style="color: var(--text-secondary); margin: 8px 0 0 0;">
Witaj, <strong>{{ current_user.name }}</strong>! Witaj, <strong>{{ current_user.name }}</strong>!
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<span class="badge-admin">Administrator</span> <span class="badge-admin">Administrator</span>
{% endif %} {% endif %}
</p> </p>
@ -508,7 +508,7 @@
</div> </div>
{% endif %} {% endif %}
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<!-- Admin Section --> <!-- Admin Section -->
<div class="admin-section-highlight"> <div class="admin-section-highlight">
<h4 style="margin: 0 0 var(--spacing-lg, 20px) 0; display: flex; align-items: center; gap: 8px;"> <h4 style="margin: 0 0 var(--spacing-lg, 20px) 0; display: flex; align-items: center; gap: 8px;">
@ -660,7 +660,7 @@
<p style="margin: 0 0 4px 0;"><strong>Email:</strong> {{ current_user.email }}</p> <p style="margin: 0 0 4px 0;"><strong>Email:</strong> {{ current_user.email }}</p>
<p style="margin: 0;"> <p style="margin: 0;">
<strong>Rola:</strong> <strong>Rola:</strong>
{% if current_user.is_admin %} {% if current_user.can_access_admin_panel() %}
<span class="badge-admin">Administrator</span> <span class="badge-admin">Administrator</span>
{% else %} {% else %}
<span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem;">Użytkownik</span> <span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem;">Użytkownik</span>

View File

@ -986,7 +986,7 @@
</span> </span>
{{ topic.title }} {{ topic.title }}
</h1> </h1>
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_moderate_forum() %}
<div class="admin-actions"> <div class="admin-actions">
<button type="button" class="admin-btn admin-btn-pin" onclick="togglePin({{ topic.id }})" title="{% if topic.is_pinned %}Odepnij{% else %}Przypnij{% endif %}"> <button type="button" class="admin-btn admin-btn-pin" onclick="togglePin({{ topic.id }})" title="{% if topic.is_pinned %}Odepnij{% else %}Przypnij{% endif %}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
@ -1090,7 +1090,7 @@
<!-- User actions for topic --> <!-- User actions for topic -->
{% if not topic.is_locked %} {% if not topic.is_locked %}
<div class="user-actions"> <div class="user-actions">
{% if topic.author_id == current_user.id or current_user.is_admin %} {% if topic.author_id == current_user.id or current_user.can_moderate_forum() %}
<button type="button" class="action-btn" onclick="openEditModal('topic', {{ topic.id }}, document.getElementById('topicContent').innerText)"> <button type="button" class="action-btn" onclick="openEditModal('topic', {{ topic.id }}, document.getElementById('topicContent').innerText)">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edytuj Edytuj
@ -1151,7 +1151,7 @@
<span class="edited-badge">(edytowano)</span> <span class="edited-badge">(edytowano)</span>
{% endif %} {% endif %}
</span> </span>
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_moderate_forum() %}
<div class="reply-admin-actions"> <div class="reply-admin-actions">
<button type="button" class="admin-btn admin-btn-sm" onclick="toggleSolution({{ reply.id }})" title="{% if reply.is_solution %}Usuń oznaczenie{% else %}Oznacz jako rozwiązanie{% endif %}"> <button type="button" class="admin-btn admin-btn-sm" onclick="toggleSolution({{ reply.id }})" title="{% if reply.is_solution %}Usuń oznaczenie{% else %}Oznacz jako rozwiązanie{% endif %}">

View File

@ -263,7 +263,7 @@
</div> </div>
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<div class="release-date">{{ release.date }}</div> <div class="release-date">{{ release.date }}</div>
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<button class="notify-btn" onclick="notifyRelease('{{ release.version }}', this)" title="Wyślij powiadomienia o tej wersji"> <button class="notify-btn" onclick="notifyRelease('{{ release.version }}', this)" title="Wyślij powiadomienia o tej wersji">
🔔 Powiadom 🔔 Powiadom
</button> </button>
@ -472,7 +472,7 @@ document.getElementById('confirmModal').addEventListener('click', function(e) {
} }
}); });
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
function notifyRelease(version, btn) { function notifyRelease(version, btn) {
showConfirmModal( showConfirmModal(
'Wyślij powiadomienia', 'Wyślij powiadomienia',

View File

@ -160,6 +160,32 @@ def company_permission(permission_type='edit'):
return decorator return decorator
def office_manager_required(f):
"""
Decorator that requires user to be at least OFFICE_MANAGER.
Shortcut for @role_required(SystemRole.OFFICE_MANAGER).
Usage:
@bp.route('/admin/companies')
@login_required
@office_manager_required
def admin_companies():
...
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('auth.login'))
SystemRole = _get_system_role()
if not current_user.has_role(SystemRole.OFFICE_MANAGER):
flash('Ta strona wymaga uprawnień kierownika biura.', 'error')
return redirect(url_for('public.index'))
return f(*args, **kwargs)
return decorated_function
def forum_access_required(f): def forum_access_required(f):
""" """
Decorator that requires user to have forum access. Decorator that requires user to have forum access.