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
def test_error_500():
"""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')
return redirect(url_for('index'))
# 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. -->
*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>

View File

@ -23,8 +23,10 @@ from werkzeug.security import generate_password_hash
from . import bp
from database import (
SessionLocal, User, Company, CompanyRecommendation,
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee,
SystemRole
)
from utils.decorators import role_required
import gemini_service
# Logger
@ -44,12 +46,9 @@ MONTHS_PL = [
@bp.route('/recommendations')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendations():
"""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()
try:
recommendations = db.query(CompanyRecommendation).order_by(
@ -86,11 +85,9 @@ def admin_recommendations():
@bp.route('/recommendations/<int:recommendation_id>/approve', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendation_approve(recommendation_id):
"""Approve a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
recommendation = db.query(CompanyRecommendation).filter(
@ -117,11 +114,9 @@ def admin_recommendation_approve(recommendation_id):
@bp.route('/recommendations/<int:recommendation_id>/reject', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_recommendation_reject(recommendation_id):
"""Reject a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
recommendation = db.query(CompanyRecommendation).filter(
@ -154,12 +149,9 @@ def admin_recommendation_reject(recommendation_id):
@bp.route('/users')
@login_required
@role_required(SystemRole.ADMIN)
def admin_users():
"""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()
try:
users = db.query(User).order_by(User.created_at.desc()).all()
@ -187,11 +179,9 @@ def admin_users():
@bp.route('/users/add', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_add():
"""Create a new user (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -241,11 +231,9 @@ def admin_user_add():
@bp.route('/users/<int:user_id>/toggle-admin', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_toggle_admin(user_id):
"""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:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_toggle_verified(user_id):
"""Toggle verified status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_update(user_id):
"""Update user data (name, email)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_assign_company(user_id):
"""Assign a company to a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_delete(user_id):
"""Delete a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
if user_id == current_user.id:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_user_reset_password(user_id):
"""Generate password reset token for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
@ -458,12 +436,9 @@ def admin_user_reset_password(user_id):
@bp.route('/fees')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees():
"""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()
try:
from sqlalchemy import func, case
@ -545,11 +520,9 @@ def admin_fees():
@bp.route('/fees/generate', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_generate():
"""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()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_mark_paid(fee_id):
"""Mark a fee as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_bulk_mark_paid():
"""Bulk mark fees as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal()
try:
fee_ids = request.form.getlist('fee_ids[]', type=int)
@ -686,12 +655,9 @@ def admin_fees_bulk_mark_paid():
@bp.route('/fees/export')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_fees_export():
"""Export fees to CSV"""
if not current_user.is_admin:
flash('Brak uprawnien.', 'error')
return redirect(url_for('.admin_fees'))
db = SessionLocal()
try:
year = request.args.get('year', datetime.now().year, type=int)
@ -747,12 +713,9 @@ def admin_fees_export():
@bp.route('/kalendarz')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar():
"""Panel admin - zarządzanie wydarzeniami"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar_new():
"""Dodaj nowe wydarzenie"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
if request.method == 'POST':
db = SessionLocal()
try:
@ -803,11 +763,9 @@ def admin_calendar_new():
@bp.route('/kalendarz/<int:event_id>/delete', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_calendar_delete(event_id):
"""Usuń wydarzenie"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_notify_release():
"""
Send notifications to all users about a new release.
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 {}
version = data.get('version')
highlights = data.get('highlights', [])

View File

@ -18,8 +18,10 @@ from sqlalchemy.orm import joinedload
from . import bp
from database import (
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__)
@ -30,12 +32,9 @@ logger = logging.getLogger(__name__)
@bp.route('/analytics')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_analytics():
"""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')
user_id = request.args.get('user_id', type=int)
@ -266,12 +265,9 @@ def admin_analytics():
@bp.route('/analytics/export')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_analytics_export():
"""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')
period = request.args.get('period', 'month')
@ -373,12 +369,9 @@ def admin_analytics_export():
@bp.route('/ai-usage')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_ai_usage():
"""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
# Get period filter from query params
@ -601,12 +594,9 @@ def admin_ai_usage():
@bp.route('/ai-usage/user/<int:user_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_ai_usage_user(user_id):
"""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()
try:
# 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 . import bp
from database import SessionLocal, Announcement
from database import SessionLocal, Announcement, SystemRole
from utils.decorators import role_required
logger = logging.getLogger(__name__)
@ -48,12 +49,9 @@ def generate_slug(title):
@bp.route('/announcements')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements():
"""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()
try:
# Filters
@ -92,12 +90,9 @@ def admin_announcements():
@bp.route('/announcements/new', methods=['GET', 'POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_new():
"""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':
db = SessionLocal()
try:
@ -174,12 +169,9 @@ def admin_announcements_new():
@bp.route('/announcements/<int:id>/edit', methods=['GET', 'POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_edit(id):
"""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()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_publish(id):
"""Publikacja ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_archive(id):
"""Archiwizacja ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_announcements_delete(id):
"""Usunięcie ogłoszenia"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
announcement = db.query(Announcement).filter(Announcement.id == id).first()

View File

@ -15,8 +15,9 @@ from . import bp
from database import (
SessionLocal, Company, Category, CompanyWebsiteAnalysis, GBPAudit,
CompanyDigitalMaturity, KRSAudit, CompanyPKD, CompanyPerson,
ITAudit, ITCollaborationMatch
ITAudit, ITCollaborationMatch, SystemRole
)
from utils.decorators import role_required
logger = logging.getLogger(__name__)
@ -27,6 +28,7 @@ logger = logging.getLogger(__name__)
@bp.route('/seo')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_seo():
"""
Admin dashboard for SEO metrics overview.
@ -42,10 +44,6 @@ def admin_seo():
Query Parameters:
- 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
filter_company_slug = request.args.get('company', '')
@ -145,6 +143,7 @@ def admin_seo():
@bp.route('/gbp-audit')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_gbp_audit():
"""
Admin dashboard for GBP (Google Business Profile) audit overview.
@ -155,10 +154,6 @@ def admin_gbp_audit():
- Review metrics (avg rating, review counts)
- Photo statistics
"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal()
try:
from sqlalchemy import func
@ -309,12 +304,9 @@ def admin_gbp_audit():
@bp.route('/digital-maturity')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def digital_maturity_dashboard():
"""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()
try:
from sqlalchemy import func, desc
@ -386,6 +378,7 @@ def digital_maturity_dashboard():
@bp.route('/krs-audit')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_krs_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
- 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
try:
from krs_audit_service import KRS_AUDIT_AVAILABLE
@ -496,6 +485,7 @@ def admin_krs_audit():
@bp.route('/it-audit')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_it_audit():
"""
Admin dashboard for IT audit overview.
@ -507,12 +497,8 @@ def admin_it_audit():
- Company table with IT audit data
- 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()
try:
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 . 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 = logging.getLogger(__name__)
@ -37,12 +38,9 @@ def validate_nip(nip: str) -> bool:
@bp.route('/companies')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_companies():
"""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()
try:
# Get filter parameters
@ -104,11 +102,9 @@ def admin_companies():
@bp.route('/companies/add', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_add():
"""Create a new company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -174,11 +170,9 @@ def admin_company_add():
@bp.route('/companies/<int:company_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_get(company_id):
"""Get company details (JSON)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_update(company_id):
"""Update company data"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_toggle_status(company_id):
"""Toggle company status (active <-> inactive)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_delete(company_id):
"""Soft delete company (set status to archived)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_assign_user(company_id):
"""Assign a user to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -373,11 +359,9 @@ def admin_company_assign_user(company_id):
@bp.route('/companies/<int:company_id>/people')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_people(company_id):
"""Get people associated with a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_users(company_id):
"""Get users assigned to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
@ -446,12 +428,9 @@ def admin_company_users(company_id):
@bp.route('/companies/export')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_companies_export():
"""Export companies to CSV"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
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_login import login_required, current_user
from database import SystemRole
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
@ -21,22 +23,17 @@ logger = logging.getLogger(__name__)
@bp.route('/insights')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_insights():
"""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')
@bp.route('/insights-api', methods=['GET'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_get_insights():
"""Get development insights for roadmap"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
from norda_knowledge_service import 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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_update_insight_status(insight_id):
"""Update insight status (for roadmap planning)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
from norda_knowledge_service import 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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_sync_insights():
"""Manually trigger knowledge sync from forum and chat"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service()
@ -121,11 +114,9 @@ def api_sync_insights():
@bp.route('/insights-api/stats', methods=['GET'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_insights_stats():
"""Get knowledge base statistics"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
from norda_knowledge_service import get_knowledge_service
service = get_knowledge_service()
@ -152,11 +143,9 @@ def api_insights_stats():
@bp.route('/ai-learning-status')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_ai_learning_status():
"""API: Get AI feedback learning status and examples"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
try:
from feedback_learning_service import get_feedback_learning_service
service = get_feedback_learning_service()
@ -211,11 +200,9 @@ def api_ai_learning_status():
@bp.route('/chat-stats')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_chat_stats():
"""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 database import SessionLocal, AIChatMessage

View File

@ -19,8 +19,10 @@ from database import (
CompanyPerson,
CompanyPKD,
CompanyFinancialReport,
KRSAudit
KRSAudit,
SystemRole
)
from utils.decorators import role_required
from . import bp
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_trigger():
"""
API: Trigger KRS audit for a company (admin-only).
@ -134,12 +137,6 @@ def api_krs_audit_trigger():
- Success: Audit results saved to database
- 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():
return jsonify({
'success': False,
@ -350,6 +347,7 @@ def api_krs_audit_trigger():
@bp.route('/krs-api/audit/batch', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_batch():
"""
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.
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():
return jsonify({
'success': False,

View File

@ -16,9 +16,11 @@ from sqlalchemy.orm.attributes import flag_modified
from . import bp
from database import (
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 utils.decorators import role_required
logger = logging.getLogger(__name__)
@ -156,12 +158,9 @@ def _enrich_company_from_krs(company, db):
@bp.route('/membership')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership():
"""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()
try:
# Get filter parameters
@ -224,12 +223,9 @@ def admin_membership():
@bp.route('/membership/<int:app_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_detail(app_id):
"""View membership application details."""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_approve(app_id):
"""Approve membership application and create company."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_reject(app_id):
"""Reject membership application."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_request_changes(app_id):
"""Request changes to membership application."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_start_review(app_id):
"""Mark application as under review."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_propose_changes(app_id):
"""
Propose changes from registry data for user approval.
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()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_membership_update_from_registry(app_id):
"""
[DEPRECATED - use propose-changes instead]
Direct update is now replaced by propose-changes workflow.
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
data = request.get_json() or {}
return admin_membership_propose_changes.__wrapped__(app_id)
@ -671,12 +655,9 @@ def admin_membership_update_from_registry(app_id):
@bp.route('/company-requests')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_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()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_request_approve(req_id):
"""Approve company data request and update company."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_company_request_reject(req_id):
"""Reject company data request."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data_request = db.query(CompanyDataRequest).get(req_id)

View File

@ -28,8 +28,10 @@ from database import (
GBPAudit,
NordaEvent,
SessionLocal,
SystemRole,
ZOPKNews,
)
from utils.decorators import role_required
from . import bp
@ -38,11 +40,9 @@ logger = logging.getLogger(__name__)
@bp.route('/model-comparison')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_model_comparison():
"""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
results_file = '/tmp/nordabiz_model_comparison_results.json'
@ -68,11 +68,9 @@ def admin_model_comparison():
@bp.route('/model-comparison/run', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_model_comparison_run():
"""Run model comparison simulation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
# Questions to compare (from real conversations)
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 . 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 = logging.getLogger(__name__)
@ -25,12 +26,9 @@ logger = logging.getLogger(__name__)
@bp.route('/people')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_people():
"""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()
try:
# Get search query
@ -114,11 +112,9 @@ def admin_people():
@bp.route('/people/add', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_add():
"""Create a new person"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -171,11 +167,9 @@ def admin_person_add():
@bp.route('/people/<int:person_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_get(person_id):
"""Get person details (JSON)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_update(person_id):
"""Update person data"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_delete(person_id):
"""Delete person (hard delete with CASCADE on CompanyPerson)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_companies(person_id):
"""Get companies associated with a person"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_link_company(person_id):
"""Link person to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_person_unlink_company(person_id, company_id):
"""Remove person-company link"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -439,11 +423,9 @@ def admin_person_unlink_company(person_id, company_id):
@bp.route('/people/search')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_people_search():
"""Search people for autocomplete"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
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 . 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__)
@ -34,12 +35,9 @@ except ImportError:
@bp.route('/security')
@login_required
@role_required(SystemRole.ADMIN)
def admin_security():
"""Security dashboard - audit logs, alerts, GeoIP stats"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal()
try:
from sqlalchemy import func, desc
@ -158,11 +156,9 @@ def admin_security():
@bp.route('/security/alert/<int:alert_id>/acknowledge', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def acknowledge_security_alert(alert_id):
"""Acknowledge a security alert"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal()
try:
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'])
@login_required
@role_required(SystemRole.ADMIN)
def resolve_security_alert(alert_id):
"""Resolve a security alert"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
note = request.form.get('note', '')
db = SessionLocal()
@ -219,11 +213,9 @@ def resolve_security_alert(alert_id):
@bp.route('/security/unlock-account/<int:user_id>', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def unlock_account(user_id):
"""Unlock a locked user account"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal()
try:
user = db.query(User).get(user_id)
@ -246,11 +238,9 @@ def unlock_account(user_id):
@bp.route('/security/geoip-stats')
@login_required
@role_required(SystemRole.ADMIN)
def api_geoip_stats():
"""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
db = SessionLocal()

View File

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

View File

@ -18,8 +18,9 @@ from sqlalchemy import func, text
from . import bp
from database import (
SessionLocal, Company, User, AuditLog, SecurityAlert,
CompanySocialMedia, CompanyWebsiteAnalysis
CompanySocialMedia, CompanyWebsiteAnalysis, SystemRole
)
from utils.decorators import role_required
logger = logging.getLogger(__name__)
@ -30,12 +31,9 @@ logger = logging.getLogger(__name__)
@bp.route('/status')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_status():
"""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()
try:
# Current timestamp
@ -535,11 +533,9 @@ def admin_status():
@bp.route('/api/status')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_admin_status():
"""API endpoint for status dashboard auto-refresh"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
db = SessionLocal()
try:
now = datetime.now()
@ -611,15 +607,12 @@ def api_admin_status():
@bp.route('/health')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_health():
"""
Graphical health check dashboard.
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
results = []
@ -748,11 +741,9 @@ def admin_health():
@bp.route('/api/health')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_admin_health():
"""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
# Run the same checks as admin_health but return JSON
@ -803,21 +794,17 @@ def api_admin_health():
@bp.route('/debug')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def debug_panel():
"""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')
@bp.route('/api/logs')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_get_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
from app import debug_handler
@ -848,11 +835,9 @@ def api_get_logs():
@bp.route('/api/logs/stream')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_logs_stream():
"""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
import time
@ -873,11 +858,9 @@ def api_logs_stream():
@bp.route('/api/logs/clear', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_clear_logs():
"""API: Clear log buffer"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Not authorized'}), 403
from app import debug_handler
debug_handler.logs.clear()
logger.info("Log buffer cleared by admin")
@ -886,11 +869,9 @@ def api_clear_logs():
@bp.route('/api/test-log', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_test_log():
"""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.info("Test INFO 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 database import SessionLocal, User, Company, SystemRole, CompanyRole, UserCompanyPermissions
from utils.decorators import role_required
import gemini_service
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'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_users_ai_parse():
"""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()
try:
# Get list of companies for AI context
@ -221,11 +220,9 @@ def admin_users_ai_parse():
@bp.route('/users-api/bulk-create', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_users_bulk_create():
"""Create multiple users from confirmed proposals."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -309,11 +306,9 @@ def admin_users_bulk_create():
@bp.route('/users-api/change-role', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_users_change_role():
"""Change user's system role."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
@ -389,11 +384,9 @@ def admin_users_change_role():
@bp.route('/users-api/roles', methods=['GET'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_users_get_roles():
"""Get list of available roles for dropdown."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
roles = [
{'value': 'UNAFFILIATED', 'label': 'Niezrzeszony', 'description': 'Firma spoza Izby'},
{'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 (
SessionLocal,
SystemRole,
ZOPKProject,
ZOPKStakeholder,
ZOPKNews,
ZOPKResource,
ZOPKNewsFetchJob
)
from utils.decorators import role_required
from . import bp
@bp.route('/zopk')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk():
"""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()
try:
# Pagination and filtering parameters

View File

@ -18,11 +18,13 @@ from sqlalchemy import text, func, distinct
from database import (
SessionLocal,
SystemRole,
ZOPKNews,
ZOPKKnowledgeChunk,
ZOPKKnowledgeEntity,
ZOPKKnowledgeEntityMention
)
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
@ -35,6 +37,7 @@ _GRAPH_CACHE_TTL = 300 # 5 minutes
@bp.route('/zopk/knowledge/stats')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_stats():
"""
Get knowledge extraction statistics.
@ -44,9 +47,6 @@ def admin_zopk_knowledge_stats():
- knowledge_base: stats about chunks, facts, entities, relations
- 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
db = SessionLocal()
@ -65,6 +65,7 @@ def admin_zopk_knowledge_stats():
@bp.route('/zopk/knowledge/extract', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract():
"""
Batch extract knowledge from scraped articles.
@ -77,9 +78,6 @@ def admin_zopk_knowledge_extract():
- chunks/facts/entities/relations created
- errors list
"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_knowledge_service import ZOPKKnowledgeService
db = SessionLocal()
@ -114,13 +112,11 @@ def admin_zopk_knowledge_extract():
@bp.route('/zopk/knowledge/extract/<int:news_id>', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract_single(news_id):
"""
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
db = SessionLocal()
@ -155,6 +151,7 @@ def admin_zopk_knowledge_extract_single(news_id):
@bp.route('/zopk/knowledge/embeddings', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_generate_embeddings():
"""
Generate embeddings for chunks that don't have them.
@ -162,9 +159,6 @@ def admin_zopk_generate_embeddings():
Request JSON:
- 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
db = SessionLocal()
@ -192,6 +186,7 @@ def admin_zopk_generate_embeddings():
@bp.route('/zopk/knowledge/extract/stream', methods=['GET'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_extract_stream():
"""
SSE endpoint for streaming knowledge extraction progress.
@ -199,9 +194,6 @@ def admin_zopk_knowledge_extract_stream():
Query params:
- 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)
user_id = current_user.id
@ -275,6 +267,7 @@ def admin_zopk_knowledge_extract_stream():
@bp.route('/zopk/knowledge/embeddings/stream', methods=['GET'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_embeddings_stream():
"""
SSE endpoint for streaming embeddings generation progress.
@ -282,9 +275,6 @@ def admin_zopk_embeddings_stream():
Query params:
- 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)
user_id = current_user.id
@ -413,28 +403,22 @@ def api_zopk_knowledge_search():
@bp.route('/zopk/knowledge')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_dashboard():
"""
Dashboard for ZOPK Knowledge Base management.
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')
@bp.route('/zopk/knowledge/chunks')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_chunks():
"""
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
# Get query params
@ -478,14 +462,11 @@ def admin_zopk_knowledge_chunks():
@bp.route('/zopk/knowledge/facts')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_facts():
"""
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
# Get query params
@ -528,14 +509,11 @@ def admin_zopk_knowledge_facts():
@bp.route('/zopk/knowledge/entities')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_entities():
"""
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
# Get query params
@ -578,11 +556,9 @@ def admin_zopk_knowledge_entities():
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_detail(chunk_id):
"""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
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'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_verify(chunk_id):
"""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
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'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_verify(fact_id):
"""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
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'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_entity_verify(entity_id):
"""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
db = SessionLocal()
@ -676,11 +646,9 @@ def api_zopk_entity_verify(entity_id):
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>', methods=['DELETE'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_chunk_delete(chunk_id):
"""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
db = SessionLocal()
@ -699,12 +667,9 @@ def api_zopk_chunk_delete(chunk_id):
@bp.route('/zopk/knowledge/duplicates')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_duplicates():
"""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
db = SessionLocal()
@ -737,11 +702,9 @@ def admin_zopk_knowledge_duplicates():
@bp.route('/zopk-api/knowledge/duplicates/preview', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_duplicates_preview():
"""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
db = SessionLocal()
@ -764,11 +727,9 @@ def api_zopk_duplicates_preview():
@bp.route('/zopk-api/knowledge/duplicates/merge', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_duplicates_merge():
"""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
db = SessionLocal()
@ -789,17 +750,15 @@ def api_zopk_duplicates_merge():
@bp.route('/zopk/knowledge/graph')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_knowledge_graph():
"""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')
@bp.route('/zopk-api/knowledge/graph/data')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_knowledge_graph_data():
"""Get graph data for entity co-occurrence visualization.
@ -808,9 +767,6 @@ def api_zopk_knowledge_graph_data():
"""
global _graph_cache
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Build cache key from parameters
entity_type = request.args.get('entity_type', '')
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')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_fact_duplicates():
"""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')
@bp.route('/zopk-api/knowledge/fact-duplicates')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_duplicates():
"""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
db = SessionLocal()
try:
@ -955,11 +907,9 @@ def api_zopk_fact_duplicates():
@bp.route('/zopk-api/knowledge/fact-duplicates/merge', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_fact_merge():
"""API - merge duplikatów faktów."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import merge_facts
db = SessionLocal()
try:
@ -978,11 +928,9 @@ def api_zopk_fact_merge():
@bp.route('/zopk-api/knowledge/auto-verify/entities', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_entities():
"""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
db = SessionLocal()
try:
@ -1000,11 +948,9 @@ def api_zopk_auto_verify_entities():
@bp.route('/zopk-api/knowledge/auto-verify/facts', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_facts():
"""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
db = SessionLocal()
try:
@ -1022,11 +968,9 @@ def api_zopk_auto_verify_facts():
@bp.route('/zopk-api/knowledge/auto-verify/similar', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_auto_verify_similar():
"""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
db = SessionLocal()
try:
@ -1044,11 +988,9 @@ def api_zopk_auto_verify_similar():
@bp.route('/zopk-api/knowledge/suggest-similar-facts')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_suggest_similar_facts():
"""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
db = SessionLocal()
try:
@ -1069,11 +1011,9 @@ def api_zopk_suggest_similar_facts():
@bp.route('/zopk-api/knowledge/dashboard-stats')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_dashboard_stats():
"""API - statystyki dashboardu."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
from zopk_knowledge_service import get_knowledge_dashboard_stats
db = SessionLocal()
try:

View File

@ -18,10 +18,12 @@ from sqlalchemy.sql import nullslast
from database import (
SessionLocal,
SystemRole,
ZOPKProject,
ZOPKNews,
ZOPKNewsFetchJob
)
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
@ -29,11 +31,9 @@ logger = logging.getLogger(__name__)
@bp.route('/zopk/news')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news():
"""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()
try:
@ -90,10 +90,9 @@ def admin_zopk_news():
@bp.route('/zopk/news/<int:news_id>/approve', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_approve(news_id):
"""Approve a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
@ -119,10 +118,9 @@ def admin_zopk_news_approve(news_id):
@bp.route('/zopk/news/<int:news_id>/reject', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_reject(news_id):
"""Reject a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
@ -152,10 +150,9 @@ def admin_zopk_news_reject(news_id):
@bp.route('/zopk/news/add', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_add():
"""Manually add a ZOPK news item"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
@ -233,10 +230,9 @@ def admin_zopk_news_add():
@bp.route('/zopk/news/reject-old', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reject_old_news():
"""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()
try:
@ -278,10 +274,9 @@ def admin_zopk_reject_old_news():
@bp.route('/zopk/news/star-counts')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_star_counts():
"""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()
try:
@ -318,10 +313,9 @@ def admin_zopk_news_star_counts():
@bp.route('/zopk/news/reject-by-stars', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reject_by_stars():
"""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()
try:
@ -383,10 +377,9 @@ def admin_zopk_reject_by_stars():
@bp.route('/zopk/news/evaluate-ai', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_evaluate_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
@ -418,10 +411,9 @@ def admin_zopk_evaluate_ai():
@bp.route('/zopk/news/reevaluate-scores', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reevaluate_scores():
"""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
@ -453,6 +445,7 @@ def admin_zopk_reevaluate_scores():
@bp.route('/zopk/news/reevaluate-low-scores', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_reevaluate_low_scores():
"""
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
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
db = SessionLocal()
@ -496,6 +486,7 @@ def admin_zopk_reevaluate_low_scores():
@bp.route('/zopk-api/search-news', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_search_news():
"""
Search for ZOPK news using multiple sources with cross-verification.
@ -509,9 +500,6 @@ def api_zopk_search_news():
- 1 source pending (manual review)
- 3+ sources auto_approved
"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
from zopk_news_service import ZOPKNewsService
db = SessionLocal()
@ -594,6 +582,7 @@ def api_zopk_search_news():
@bp.route('/zopk/news/scrape-stats')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_stats():
"""
Get content scraping statistics.
@ -606,9 +595,6 @@ def admin_zopk_scrape_stats():
- skipped: Skipped (social media, paywalls)
- 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
db = SessionLocal()
@ -627,6 +613,7 @@ def admin_zopk_scrape_stats():
@bp.route('/zopk/news/scrape-content', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_content():
"""
Batch scrape article content from source URLs.
@ -642,9 +629,6 @@ def admin_zopk_scrape_content():
- errors: list of error details
- 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
db = SessionLocal()
@ -673,13 +657,11 @@ def admin_zopk_scrape_content():
@bp.route('/zopk/news/<int:news_id>/scrape', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_scrape_single(news_id):
"""
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
db = SessionLocal()
@ -711,6 +693,7 @@ def admin_zopk_scrape_single(news_id):
@bp.route('/zopk/news/scrape-content/stream', methods=['GET'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_news_scrape_stream():
"""
Stream scraping progress using Server-Sent Events.
@ -719,9 +702,6 @@ def admin_zopk_news_scrape_stream():
- limit: int (default 50)
- 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
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 (
SessionLocal,
SystemRole,
ZOPKMilestone
)
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
@ -22,11 +24,9 @@ logger = logging.getLogger(__name__)
@bp.route('/zopk/timeline')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk_timeline():
"""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')
@ -58,10 +58,9 @@ def api_zopk_milestones():
@bp.route('/zopk-api/milestones', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_create():
"""API - utworzenie kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal()
try:
@ -88,10 +87,9 @@ def api_zopk_milestone_create():
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['PUT'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_update(milestone_id):
"""API - aktualizacja kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal()
try:
@ -126,10 +124,9 @@ def api_zopk_milestone_update(milestone_id):
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['DELETE'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_milestone_delete(milestone_id):
"""API - usunięcie kamienia milowego."""
if not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
db = SessionLocal()
try:
@ -149,10 +146,9 @@ def api_zopk_milestone_delete(milestone_id):
@bp.route('/zopk-api/timeline/suggestions')
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_timeline_suggestions():
"""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
@ -177,10 +173,9 @@ def api_zopk_timeline_suggestions():
@bp.route('/zopk-api/timeline/suggestions/approve', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def api_zopk_timeline_suggestion_approve():
"""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

View File

@ -511,9 +511,9 @@ def api_enrich_company_ai(company_id):
'error': 'Firma nie znaleziona'
}), 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}")
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. Tylko administrator lub wlasciciel firmy moze wzbogacac dane.'
@ -755,8 +755,8 @@ def api_get_proposals(company_id):
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions
if not current_user.is_admin and current_user.company_id != company.id:
# Check permissions - user with company edit rights
if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposals = db.query(AiEnrichmentProposal).filter_by(
@ -798,8 +798,8 @@ def api_approve_proposal(company_id, proposal_id):
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions - only admin or company owner
if not current_user.is_admin and current_user.company_id != company.id:
# Check permissions - user with company edit rights
if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposal = db.query(AiEnrichmentProposal).filter_by(
@ -904,8 +904,8 @@ def api_reject_proposal(company_id, proposal_id):
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Check permissions
if not current_user.is_admin and current_user.company_id != company.id:
# Check permissions - user with company edit rights
if not current_user.can_edit_company(company.id):
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
proposal = db.query(AiEnrichmentProposal).filter_by(
@ -972,7 +972,7 @@ def test_sanitization():
Admin API: Test sensitive data detection without saving.
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
try:

View File

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

View File

@ -233,8 +233,8 @@ def api_edit_recommendation(rec_id):
'error': 'Rekomendacja nie znaleziona'
}), 404
# Check authorization - user must be the owner OR admin
if recommendation.user_id != current_user.id and not current_user.is_admin:
# Check authorization - user must be the owner OR have admin panel access
if recommendation.user_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({
'success': False,
'error': 'Brak uprawnień do edycji tej rekomendacji'
@ -313,8 +313,8 @@ def api_delete_recommendation(rec_id):
'error': 'Rekomendacja nie znaleziona'
}), 404
# Check authorization - user must be the owner OR admin
if recommendation.user_id != current_user.id and not current_user.is_admin:
# Check authorization - user must be the owner OR have admin panel access
if recommendation.user_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({
'success': False,
'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
- Error: Error message with status code
"""
# Admin-only check
if not current_user.is_admin:
# Check admin panel access
if not current_user.can_access_admin_panel():
return jsonify({
'success': False,
'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.'
}), 404
# Access control - admin can audit all, users only their company
if not current_user.is_admin:
if current_user.company_id != company.id:
return jsonify({
'success': False,
'error': 'Brak uprawnień do audytu social media tej firmy.'
}), 403
# Access control - users with admin panel access or company edit rights can audit
if not current_user.can_edit_company(company.id):
return jsonify({
'success': False,
'error': 'Brak uprawnień do audytu social media tej firmy.'
}), 403
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')
return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own
if not current_user.is_admin:
if current_user.company_id != company.id:
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest SEO analysis for this company
analysis = db.query(CompanyWebsiteAnalysis).filter(
@ -90,8 +89,8 @@ def seo_audit_dashboard(slug):
'url': analysis.website_url
}
# Determine if user can run audit (admin or company owner)
can_audit = current_user.is_admin or current_user.company_id == company.id
# Determine if user can run audit (user with company edit rights)
can_audit = current_user.can_edit_company(company.id)
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')
return redirect(url_for('dashboard'))
# Access control - admin can view all, users only their company
if not current_user.is_admin:
if current_user.company_id != company.id:
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
return redirect(url_for('dashboard'))
# Access control - users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get social media profiles for this company
social_profiles = db.query(CompanySocialMedia).filter(
@ -179,8 +177,8 @@ def social_audit_dashboard(slug):
'score': score
}
# Determine if user can run audit (admin or company owner)
can_audit = current_user.is_admin or current_user.company_id == company.id
# Determine if user can run audit (user with company edit rights)
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}")
@ -233,11 +231,10 @@ def gbp_audit_dashboard(slug):
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own
if not current_user.is_admin:
if current_user.company_id != company.id:
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest audit for this company
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)
# The user can trigger an audit from the dashboard
# Determine if user can run audit (admin or company owner)
can_audit = current_user.is_admin or current_user.company_id == company.id
# Determine if user can run audit (user with company edit rights)
can_audit = current_user.can_edit_company(company.id)
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')
return redirect(url_for('dashboard'))
# Access control: admin can view any company, member only their own
if not current_user.is_admin:
if current_user.company_id != company.id:
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest IT audit for this company
audit = db.query(ITAudit).filter(
@ -356,8 +352,8 @@ def it_audit_dashboard(slug):
'recommendations': audit.recommendations
}
# Determine if user can edit audit (admin or company owner)
can_edit = current_user.is_admin or current_user.company_id == company.id
# Determine if user can edit audit (user with company edit rights)
can_edit = current_user.can_edit_company(company.id)
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
def chat_analytics():
"""Admin dashboard for chat analytics"""
# Only admins can access
if not current_user.is_admin:
# Only users with admin panel access can view chat analytics
if not current_user.can_access_admin_panel():
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))

View File

@ -159,7 +159,7 @@ def view(classified_id):
questions_query = db.query(ClassifiedQuestion).filter(
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 = questions_query.order_by(ClassifiedQuestion.created_at.asc()).all()
@ -209,7 +209,7 @@ def close(classified_id):
@login_required
def delete(classified_id):
"""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
db = SessionLocal()
@ -233,7 +233,7 @@ def delete(classified_id):
@login_required
def toggle_active(classified_id):
"""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
db = SessionLocal()
@ -323,8 +323,8 @@ def list_interests(classified_id):
if not classified:
return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404
# Tylko autor może widzieć pełną listę
if classified.author_id != current_user.id and not current_user.is_admin:
# Tylko autor może widzieć pełną listę (lub admin)
if classified.author_id != current_user.id and not current_user.can_access_admin_panel():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
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
# 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
question = db.query(ClassifiedQuestion).filter(
@ -511,7 +511,7 @@ def list_questions(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)
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()
# 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))
return render_template('contacts/detail.html',
@ -213,8 +213,8 @@ def edit(contact_id):
flash('Kontakt nie został znaleziony.', 'error')
return redirect(url_for('.contacts_list'))
# Check permissions
if not current_user.is_admin and contact.created_by != current_user.id:
# Check permissions - creator or admin
if not current_user.can_access_admin_panel() and contact.created_by != current_user.id:
flash('Nie masz uprawnień do edycji tego kontaktu.', 'error')
return redirect(url_for('.contact_detail', contact_id=contact_id))
@ -282,8 +282,8 @@ def delete(contact_id):
flash('Kontakt nie został znaleziony.', 'error')
return redirect(url_for('.contacts_list'))
# Check permissions
if not current_user.is_admin and contact.created_by != current_user.id:
# Check permissions - creator or admin
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')
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')
return redirect(url_for('.forum_index'))
# Check if topic is soft-deleted (only admins can view)
if topic.is_deleted and not current_user.is_admin:
# Check if topic is soft-deleted (only moderators can view)
if topic.is_deleted and not current_user.can_moderate_forum():
flash('Temat nie istnieje.', 'error')
return redirect(url_for('.forum_index'))
@ -237,9 +237,9 @@ def forum_topic(topic_id):
if not existing_topic_read:
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
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
for reply in visible_replies:
@ -397,7 +397,7 @@ def forum_reply(topic_id):
@login_required
def admin_forum():
"""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')
return redirect(url_for('.forum_index'))
@ -451,7 +451,7 @@ def admin_forum():
@login_required
def admin_forum_pin(topic_id):
"""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
db = SessionLocal()
@ -477,7 +477,7 @@ def admin_forum_pin(topic_id):
@login_required
def admin_forum_lock(topic_id):
"""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
db = SessionLocal()
@ -503,7 +503,7 @@ def admin_forum_lock(topic_id):
@login_required
def admin_forum_delete_topic(topic_id):
"""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
db = SessionLocal()
@ -529,7 +529,7 @@ def admin_forum_delete_topic(topic_id):
@login_required
def admin_forum_delete_reply(reply_id):
"""Delete a reply"""
if not current_user.is_admin:
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -554,8 +554,8 @@ def admin_forum_delete_reply(reply_id):
@bp.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST'])
@login_required
def admin_forum_change_status(topic_id):
"""Change topic status (admin only)"""
if not current_user.is_admin:
"""Change topic status (moderators only)"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {}
@ -593,8 +593,8 @@ def admin_forum_change_status(topic_id):
@bp.route('/admin/forum/bulk-action', methods=['POST'])
@login_required
def admin_forum_bulk_action():
"""Perform bulk action on multiple topics (admin only)"""
if not current_user.is_admin:
"""Perform bulk action on multiple topics (moderators only)"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {}
@ -700,12 +700,12 @@ def edit_topic(topic_id):
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
# Check ownership (unless admin)
if topic.author_id != current_user.id and not current_user.is_admin:
# Check ownership (unless moderator)
if topic.author_id != current_user.id and not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Check time limit (unless admin)
if not current_user.is_admin and not _can_edit_content(topic.created_at):
# Check time limit (unless moderator)
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
if topic.is_locked:
@ -756,12 +756,12 @@ def edit_reply(reply_id):
if not reply:
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
# Check ownership (unless admin)
if reply.author_id != current_user.id and not current_user.is_admin:
# Check ownership (unless moderator)
if reply.author_id != current_user.id and not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
# Check time limit (unless admin)
if not current_user.is_admin and not _can_edit_content(reply.created_at):
# Check time limit (unless moderator)
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
# Check if topic is locked
@ -809,7 +809,7 @@ def delete_own_reply(reply_id):
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
# 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
# Check if topic is locked
@ -1094,8 +1094,8 @@ def report_content():
@bp.route('/admin/forum/topic/<int:topic_id>/admin-edit', methods=['POST'])
@login_required
def admin_edit_topic(topic_id):
"""Admin: Edit any topic content"""
if not current_user.is_admin:
"""Moderator: Edit any topic content"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
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'])
@login_required
def admin_edit_reply(reply_id):
"""Admin: Edit any reply content"""
if not current_user.is_admin:
"""Moderator: Edit any reply content"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
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'])
@login_required
def mark_as_solution(reply_id):
"""Admin: Mark reply as solution"""
if not current_user.is_admin:
"""Moderator: Mark reply as solution"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -1251,8 +1251,8 @@ def mark_as_solution(reply_id):
@bp.route('/admin/forum/topic/<int:topic_id>/restore', methods=['POST'])
@login_required
def restore_topic(topic_id):
"""Admin: Restore soft-deleted topic"""
if not current_user.is_admin:
"""Moderator: Restore soft-deleted topic"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -1281,8 +1281,8 @@ def restore_topic(topic_id):
@bp.route('/admin/forum/reply/<int:reply_id>/restore', methods=['POST'])
@login_required
def restore_reply(reply_id):
"""Admin: Restore soft-deleted reply"""
if not current_user.is_admin:
"""Moderator: Restore soft-deleted reply"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -1311,8 +1311,8 @@ def restore_reply(reply_id):
@bp.route('/admin/forum/reports')
@login_required
def admin_forum_reports():
"""Admin: View all reports"""
if not current_user.is_admin:
"""Moderator: View all reports"""
if not current_user.can_moderate_forum():
flash('Brak uprawnień do tej strony.', 'error')
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'])
@login_required
def review_report(report_id):
"""Admin: Review a report"""
if not current_user.is_admin:
"""Moderator: Review a report"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {}
@ -1383,8 +1383,8 @@ def review_report(report_id):
@bp.route('/admin/forum/topic/<int:topic_id>/history')
@login_required
def topic_edit_history(topic_id):
"""Admin: View topic edit history"""
if not current_user.is_admin:
"""Moderator: View topic edit history"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -1412,8 +1412,8 @@ def topic_edit_history(topic_id):
@bp.route('/admin/forum/reply/<int:reply_id>/history')
@login_required
def reply_edit_history(reply_id):
"""Admin: View reply edit history"""
if not current_user.is_admin:
"""Moderator: View reply edit history"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
@ -1441,8 +1441,8 @@ def reply_edit_history(reply_id):
@bp.route('/admin/forum/deleted')
@login_required
def admin_deleted_content():
"""Admin: View soft-deleted topics and replies"""
if not current_user.is_admin:
"""Moderator: View soft-deleted topics and replies"""
if not current_user.can_moderate_forum():
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('.forum_index'))
@ -1553,7 +1553,7 @@ def user_forum_stats(user_id):
@login_required
def admin_forum_analytics():
"""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')
return redirect(url_for('.forum_index'))
@ -1787,7 +1787,7 @@ def admin_forum_analytics():
@login_required
def admin_forum_export_activity():
"""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
from flask import Response
@ -1880,7 +1880,7 @@ def admin_forum_export_activity():
@login_required
def admin_move_topic(topic_id):
"""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
data = request.get_json() or {}
@ -1914,7 +1914,7 @@ def admin_move_topic(topic_id):
@login_required
def admin_merge_topics():
"""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
data = request.get_json() or {}
@ -1986,8 +1986,8 @@ def admin_merge_topics():
@bp.route('/admin/forum/search')
@login_required
def admin_forum_search():
"""Search all forum content (including deleted) - admin only"""
if not current_user.is_admin:
"""Search all forum content (including deleted) - moderators only"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
query = request.args.get('q', '').strip()
@ -2076,8 +2076,8 @@ def admin_forum_search():
@bp.route('/admin/forum/user/<int:user_id>/activity')
@login_required
def admin_user_forum_activity(user_id):
"""Get detailed forum activity for a specific user - admin only"""
if not current_user.is_admin:
"""Get detailed forum activity for a specific user - moderators only"""
if not current_user.can_moderate_forum():
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
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 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
flash('Wybierz firmę do przeprowadzenia audytu IT.', 'info')
return redirect(url_for('admin_it_audit'))
@ -89,8 +89,8 @@ def it_audit_form():
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control: admin can access any company, users only their own
if not current_user.is_admin and current_user.company_id != company.id:
# Access control: users with company edit rights can access
if not current_user.can_edit_company(company.id):
flash('Nie masz uprawnień do edycji audytu IT tej firmy.', 'error')
return redirect(url_for('dashboard'))
@ -193,8 +193,8 @@ def it_audit_save():
'error': 'Firma nie znaleziona lub nieaktywna.'
}), 404
# Access control: admin can save for any company, users only their own
if not current_user.is_admin and current_user.company_id != company.id:
# Access control: users with company edit rights can save
if not current_user.can_edit_company(company.id):
return jsonify({
'success': False,
'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)
- match_reason and shared_attributes
"""
# Only admins can view collaboration matches
if not current_user.is_admin:
# Only users with admin panel access can view collaboration matches
if not current_user.can_access_admin_panel():
return jsonify({
'success': False,
'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
# Access control: users can only view their own company's history
if not current_user.is_admin and current_user.company_id != company_id:
# Access control: users with company edit rights can view history
if not current_user.can_edit_company(company_id):
return jsonify({
'success': False,
'error': 'Brak uprawnień do przeglądania historii audytów tej firmy.'
@ -210,7 +210,7 @@ def api_it_audit_export():
Returns:
CSV file with IT audit data
"""
if not current_user.is_admin:
if not current_user.can_access_admin_panel():
return jsonify({
'success': False,
'error': 'Tylko administrator może eksportować dane audytów.'

View File

@ -186,10 +186,10 @@ def company_detail(company_id):
company_id=company_id
).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
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',
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"/>
</svg>
</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' }}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if has_audit %}

View File

@ -558,7 +558,7 @@
</svg>
API
</a>
{% if current_user.is_admin %}
{% if current_user.can_access_admin_panel() %}
<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">
<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"/>
</svg>
</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">
<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"/>

View File

@ -1215,7 +1215,7 @@
</div>
</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 -->
<div class="admin-bar">
<div class="admin-bar-inner">

View File

@ -375,7 +375,7 @@
</div>
{% 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>
{% endif %}
</div>

View File

@ -9,7 +9,7 @@
/* Reset dla pełnoekranowego chatu jak ChatGPT/Claude */
:root {
/* 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 {

View File

@ -628,7 +628,7 @@
{% if classified.author_id == current_user.id %}
<button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button>
{% 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">
<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">

View File

@ -740,7 +740,7 @@
{# GBP Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% 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">
<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"/>
@ -752,7 +752,7 @@
{# SEO Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% 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">
<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"/>
@ -764,7 +764,7 @@
{# Social Media Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% 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">
<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"/>
@ -776,7 +776,7 @@
{# IT Infrastructure Audit link - visible to admins (all profiles) or regular users (own company only) #}
{% 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">
<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"/>

View File

@ -388,7 +388,7 @@
</h2>
<p style="color: var(--text-secondary); margin: 8px 0 0 0;">
Witaj, <strong>{{ current_user.name }}</strong>!
{% if current_user.is_admin %}
{% if current_user.can_access_admin_panel() %}
<span class="badge-admin">Administrator</span>
{% endif %}
</p>
@ -508,7 +508,7 @@
</div>
{% endif %}
{% if current_user.is_admin %}
{% if current_user.can_access_admin_panel() %}
<!-- Admin Section -->
<div class="admin-section-highlight">
<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;">
<strong>Rola:</strong>
{% if current_user.is_admin %}
{% if current_user.can_access_admin_panel() %}
<span class="badge-admin">Administrator</span>
{% else %}
<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>
{{ topic.title }}
</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">
<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">
@ -1090,7 +1090,7 @@
<!-- User actions for topic -->
{% if not topic.is_locked %}
<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)">
<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
@ -1151,7 +1151,7 @@
<span class="edited-badge">(edytowano)</span>
{% endif %}
</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">
<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 style="display: flex; align-items: center; gap: 8px;">
<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">
🔔 Powiadom
</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) {
showConfirmModal(
'Wyślij powiadomienia',

View File

@ -160,6 +160,32 @@ def company_permission(permission_type='edit'):
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):
"""
Decorator that requires user to have forum access.