diff --git a/app.py b/app.py index 6c7c299..29feaa1 100644 --- a/app.py +++ b/app.py @@ -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 diff --git a/blueprints/admin/CLAUDE.md b/blueprints/admin/CLAUDE.md index adfdcb1..e0c4a68 100644 --- a/blueprints/admin/CLAUDE.md +++ b/blueprints/admin/CLAUDE.md @@ -3,5 +3,12 @@ -*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 | \ No newline at end of file diff --git a/blueprints/admin/routes.py b/blueprints/admin/routes.py index d85c848..6e3e4b5 100644 --- a/blueprints/admin/routes.py +++ b/blueprints/admin/routes.py @@ -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//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//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//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//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//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//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//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//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//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//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', []) diff --git a/blueprints/admin/routes_analytics.py b/blueprints/admin/routes_analytics.py index 7c24749..cc40e92 100644 --- a/blueprints/admin/routes_analytics.py +++ b/blueprints/admin/routes_analytics.py @@ -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/') @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 diff --git a/blueprints/admin/routes_announcements.py b/blueprints/admin/routes_announcements.py index 4371cab..6edd1f6 100644 --- a/blueprints/admin/routes_announcements.py +++ b/blueprints/admin/routes_announcements.py @@ -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//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//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//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//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() diff --git a/blueprints/admin/routes_audits.py b/blueprints/admin/routes_audits.py index 6cac6ba..c632a4e 100644 --- a/blueprints/admin/routes_audits.py +++ b/blueprints/admin/routes_audits.py @@ -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 diff --git a/blueprints/admin/routes_companies.py b/blueprints/admin/routes_companies.py index 9ea017c..ce6375f 100644 --- a/blueprints/admin/routes_companies.py +++ b/blueprints/admin/routes_companies.py @@ -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/') @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//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//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//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//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//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//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() diff --git a/blueprints/admin/routes_insights.py b/blueprints/admin/routes_insights.py index 0996e50..9b700da 100644 --- a/blueprints/admin/routes_insights.py +++ b/blueprints/admin/routes_insights.py @@ -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//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 diff --git a/blueprints/admin/routes_krs_api.py b/blueprints/admin/routes_krs_api.py index 2de0b0e..b920e70 100644 --- a/blueprints/admin/routes_krs_api.py +++ b/blueprints/admin/routes_krs_api.py @@ -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, diff --git a/blueprints/admin/routes_membership.py b/blueprints/admin/routes_membership.py index 9e02982..aac4a52 100644 --- a/blueprints/admin/routes_membership.py +++ b/blueprints/admin/routes_membership.py @@ -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/') @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//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//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//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//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//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//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//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//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) diff --git a/blueprints/admin/routes_model_comparison.py b/blueprints/admin/routes_model_comparison.py index adeec65..77fa3ca 100644 --- a/blueprints/admin/routes_model_comparison.py +++ b/blueprints/admin/routes_model_comparison.py @@ -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 = { diff --git a/blueprints/admin/routes_people.py b/blueprints/admin/routes_people.py index 525ce93..614438c 100644 --- a/blueprints/admin/routes_people.py +++ b/blueprints/admin/routes_people.py @@ -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/') @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//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//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//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//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//unlink-company/', 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() diff --git a/blueprints/admin/routes_security.py b/blueprints/admin/routes_security.py index 8ccb8d1..28e2c92 100644 --- a/blueprints/admin/routes_security.py +++ b/blueprints/admin/routes_security.py @@ -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//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//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/', 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() diff --git a/blueprints/admin/routes_social.py b/blueprints/admin/routes_social.py index 27ec4f0..5c60c23 100644 --- a/blueprints/admin/routes_social.py +++ b/blueprints/admin/routes_social.py @@ -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 diff --git a/blueprints/admin/routes_status.py b/blueprints/admin/routes_status.py index 9fce1f4..b134f18 100644 --- a/blueprints/admin/routes_status.py +++ b/blueprints/admin/routes_status.py @@ -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") diff --git a/blueprints/admin/routes_users_api.py b/blueprints/admin/routes_users_api.py index 2726bb9..56ccaaa 100644 --- a/blueprints/admin/routes_users_api.py +++ b/blueprints/admin/routes_users_api.py @@ -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'}, diff --git a/blueprints/admin/routes_zopk_dashboard.py b/blueprints/admin/routes_zopk_dashboard.py index f4bf2b6..ce3e68b 100644 --- a/blueprints/admin/routes_zopk_dashboard.py +++ b/blueprints/admin/routes_zopk_dashboard.py @@ -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 diff --git a/blueprints/admin/routes_zopk_knowledge.py b/blueprints/admin/routes_zopk_knowledge.py index 3230a3e..ac7620f 100644 --- a/blueprints/admin/routes_zopk_knowledge.py +++ b/blueprints/admin/routes_zopk_knowledge.py @@ -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/', 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/') @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//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//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//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/', 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: diff --git a/blueprints/admin/routes_zopk_news.py b/blueprints/admin/routes_zopk_news.py index 2bd57e2..c69a7d2 100644 --- a/blueprints/admin/routes_zopk_news.py +++ b/blueprints/admin/routes_zopk_news.py @@ -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//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//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//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) diff --git a/blueprints/admin/routes_zopk_timeline.py b/blueprints/admin/routes_zopk_timeline.py index 6d9f22b..845cc8d 100644 --- a/blueprints/admin/routes_zopk_timeline.py +++ b/blueprints/admin/routes_zopk_timeline.py @@ -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/', 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/', 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 diff --git a/blueprints/api/routes_company.py b/blueprints/api/routes_company.py index 31fc006..aac7858 100644 --- a/blueprints/api/routes_company.py +++ b/blueprints/api/routes_company.py @@ -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: diff --git a/blueprints/api/routes_gbp_audit.py b/blueprints/api/routes_gbp_audit.py index a470e91..1b5341c 100644 --- a/blueprints/api/routes_gbp_audit.py +++ b/blueprints/api/routes_gbp_audit.py @@ -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})") diff --git a/blueprints/api/routes_recommendations.py b/blueprints/api/routes_recommendations.py index 7ef90e4..12c06ca 100644 --- a/blueprints/api/routes_recommendations.py +++ b/blueprints/api/routes_recommendations.py @@ -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' diff --git a/blueprints/api/routes_seo_audit.py b/blueprints/api/routes_seo_audit.py index 487d090..34dbbe2 100644 --- a/blueprints/api/routes_seo_audit.py +++ b/blueprints/api/routes_seo_audit.py @@ -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.' diff --git a/blueprints/api/routes_social_audit.py b/blueprints/api/routes_social_audit.py index 8fadfee..1ada97e 100644 --- a/blueprints/api/routes_social_audit.py +++ b/blueprints/api/routes_social_audit.py @@ -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})") diff --git a/blueprints/audit/CLAUDE.md b/blueprints/audit/CLAUDE.md new file mode 100644 index 0000000..9a99b36 --- /dev/null +++ b/blueprints/audit/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### 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 | + \ No newline at end of file diff --git a/blueprints/audit/routes.py b/blueprints/audit/routes.py index 8d843e4..13bb615 100644 --- a/blueprints/audit/routes.py +++ b/blueprints/audit/routes.py @@ -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}") diff --git a/blueprints/chat/CLAUDE.md b/blueprints/chat/CLAUDE.md new file mode 100644 index 0000000..1c6d874 --- /dev/null +++ b/blueprints/chat/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 31, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #180 | 6:25 PM | 🔵 | Nordabiz project architecture analyzed revealing 16+ Flask blueprints with modular organization | ~831 | + \ No newline at end of file diff --git a/blueprints/chat/routes.py b/blueprints/chat/routes.py index 27e115f..63d0c10 100644 --- a/blueprints/chat/routes.py +++ b/blueprints/chat/routes.py @@ -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')) diff --git a/blueprints/community/classifieds/routes.py b/blueprints/community/classifieds/routes.py index 31d18fb..88147e6 100644 --- a/blueprints/community/classifieds/routes.py +++ b/blueprints/community/classifieds/routes.py @@ -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() diff --git a/blueprints/community/contacts/routes.py b/blueprints/community/contacts/routes.py index 059db82..77c615a 100644 --- a/blueprints/community/contacts/routes.py +++ b/blueprints/community/contacts/routes.py @@ -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)) diff --git a/blueprints/forum/routes.py b/blueprints/forum/routes.py index 40a0d58..1e71aba 100644 --- a/blueprints/forum/routes.py +++ b/blueprints/forum/routes.py @@ -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//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//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//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//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//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//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//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//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//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//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 diff --git a/blueprints/it_audit/routes.py b/blueprints/it_audit/routes.py index d075e4b..89c3f6d 100644 --- a/blueprints/it_audit/routes.py +++ b/blueprints/it_audit/routes.py @@ -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.' diff --git a/blueprints/it_audit/routes_api.py b/blueprints/it_audit/routes_api.py index 16afc79..60b43e6 100644 --- a/blueprints/it_audit/routes_api.py +++ b/blueprints/it_audit/routes_api.py @@ -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.' diff --git a/blueprints/public/routes.py b/blueprints/public/routes.py index 58b5c66..7ee0b0f 100644 --- a/blueprints/public/routes.py +++ b/blueprints/public/routes.py @@ -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, diff --git a/database/migrations/20260201_sync_user_roles.sql b/database/migrations/20260201_sync_user_roles.sql new file mode 100644 index 0000000..1a810e3 --- /dev/null +++ b/database/migrations/20260201_sync_user_roles.sql @@ -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; diff --git a/templates/admin/it_audit_dashboard.html b/templates/admin/it_audit_dashboard.html index 12eb6e8..c9049a8 100644 --- a/templates/admin/it_audit_dashboard.html +++ b/templates/admin/it_audit_dashboard.html @@ -1339,7 +1339,7 @@ - {% if current_user.is_admin %} + {% if current_user.can_access_admin_panel() %} {% if has_audit %} diff --git a/templates/admin_seo_dashboard.html b/templates/admin_seo_dashboard.html index 3720507..3da8e61 100644 --- a/templates/admin_seo_dashboard.html +++ b/templates/admin_seo_dashboard.html @@ -558,7 +558,7 @@ API - {% if current_user.is_admin %} + {% if current_user.can_access_admin_panel() %} {% endif %} - {% if current_user.is_authenticated and current_user.is_admin %} + {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
{% endif %} - {% if current_user.is_admin %} + {% if current_user.can_access_admin_panel() %}

@@ -660,7 +660,7 @@

Email: {{ current_user.email }}

Rola: - {% if current_user.is_admin %} + {% if current_user.can_access_admin_panel() %} Administrator {% else %} Użytkownik diff --git a/templates/forum/topic.html b/templates/forum/topic.html index 698a92b..54cabef 100755 --- a/templates/forum/topic.html +++ b/templates/forum/topic.html @@ -986,7 +986,7 @@ {{ topic.title }}

- {% if current_user.is_authenticated and current_user.is_admin %} + {% if current_user.is_authenticated and current_user.can_moderate_forum() %}
{{ release.date }}
- {% if current_user.is_authenticated and current_user.is_admin %} + {% if current_user.is_authenticated and current_user.can_access_admin_panel() %} @@ -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', diff --git a/utils/decorators.py b/utils/decorators.py index 8c1d93e..be4cd9e 100644 --- a/utils/decorators.py +++ b/utils/decorators.py @@ -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.