refactor: Migrate access control from is_admin to role-based system
Replace ~170 manual `if not current_user.is_admin` checks with: - @role_required(SystemRole.ADMIN) for user management, security, ZOPK - @role_required(SystemRole.OFFICE_MANAGER) for content management - current_user.can_access_admin_panel() for admin UI access - current_user.can_moderate_forum() for forum moderation - current_user.can_edit_company(id) for company permissions Add @office_manager_required decorator shortcut. Add SQL migration to sync existing users' role field. Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d90b7ec3b7
commit
4181a2e760
2
app.py
2
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
|
||||
|
||||
@ -3,5 +3,12 @@
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
### Jan 31, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #250 | 6:33 PM | 🔵 | Nordabiz admin blueprint imports 14 separate routes modules demonstrating extreme modularization | ~677 |
|
||||
| #180 | 6:25 PM | 🔵 | Nordabiz project architecture analyzed revealing 16+ Flask blueprints with modular organization | ~831 |
|
||||
| #170 | 6:23 PM | 🔵 | Nordabiz admin routes handle recommendations moderation with Polish localization | ~713 |
|
||||
| #168 | " | 🔵 | Nordabiz admin blueprint splits functionality across 15 route modules | ~726 |
|
||||
</claude-mem-context>
|
||||
@ -23,8 +23,10 @@ from werkzeug.security import generate_password_hash
|
||||
from . import bp
|
||||
from database import (
|
||||
SessionLocal, User, Company, CompanyRecommendation,
|
||||
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee
|
||||
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee,
|
||||
SystemRole
|
||||
)
|
||||
from utils.decorators import role_required
|
||||
import gemini_service
|
||||
|
||||
# Logger
|
||||
@ -44,12 +46,9 @@ MONTHS_PL = [
|
||||
|
||||
@bp.route('/recommendations')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_recommendations():
|
||||
"""Admin panel for recommendations moderation"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
recommendations = db.query(CompanyRecommendation).order_by(
|
||||
@ -86,11 +85,9 @@ def admin_recommendations():
|
||||
|
||||
@bp.route('/recommendations/<int:recommendation_id>/approve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_recommendation_approve(recommendation_id):
|
||||
"""Approve a recommendation"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
recommendation = db.query(CompanyRecommendation).filter(
|
||||
@ -117,11 +114,9 @@ def admin_recommendation_approve(recommendation_id):
|
||||
|
||||
@bp.route('/recommendations/<int:recommendation_id>/reject', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_recommendation_reject(recommendation_id):
|
||||
"""Reject a recommendation"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
recommendation = db.query(CompanyRecommendation).filter(
|
||||
@ -154,12 +149,9 @@ def admin_recommendation_reject(recommendation_id):
|
||||
|
||||
@bp.route('/users')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_users():
|
||||
"""Admin panel for user management"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
users = db.query(User).order_by(User.created_at.desc()).all()
|
||||
@ -187,11 +179,9 @@ def admin_users():
|
||||
|
||||
@bp.route('/users/add', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_add():
|
||||
"""Create a new user (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -241,11 +231,9 @@ def admin_user_add():
|
||||
|
||||
@bp.route('/users/<int:user_id>/toggle-admin', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_toggle_admin(user_id):
|
||||
"""Toggle admin status for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'error': 'Nie możesz zmienić własnych uprawnień'}), 400
|
||||
|
||||
@ -271,11 +259,9 @@ def admin_user_toggle_admin(user_id):
|
||||
|
||||
@bp.route('/users/<int:user_id>/toggle-verified', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_toggle_verified(user_id):
|
||||
"""Toggle verified status for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
@ -302,11 +288,9 @@ def admin_user_toggle_verified(user_id):
|
||||
|
||||
@bp.route('/users/<int:user_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_update(user_id):
|
||||
"""Update user data (name, email)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
@ -353,11 +337,9 @@ def admin_user_update(user_id):
|
||||
|
||||
@bp.route('/users/<int:user_id>/assign-company', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_assign_company(user_id):
|
||||
"""Assign a company to a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
@ -392,11 +374,9 @@ def admin_user_assign_company(user_id):
|
||||
|
||||
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_delete(user_id):
|
||||
"""Delete a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'error': 'Nie możesz usunąć własnego konta'}), 400
|
||||
|
||||
@ -422,11 +402,9 @@ def admin_user_delete(user_id):
|
||||
|
||||
@bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_user_reset_password(user_id):
|
||||
"""Generate password reset token for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
@ -458,12 +436,9 @@ def admin_user_reset_password(user_id):
|
||||
|
||||
@bp.route('/fees')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_fees():
|
||||
"""Admin panel for membership fee management"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnien do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from sqlalchemy import func, case
|
||||
@ -545,11 +520,9 @@ def admin_fees():
|
||||
|
||||
@bp.route('/fees/generate', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_fees_generate():
|
||||
"""Generate fee records for all companies for a given month"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
year = request.form.get('year', type=int)
|
||||
@ -601,11 +574,9 @@ def admin_fees_generate():
|
||||
|
||||
@bp.route('/fees/<int:fee_id>/mark-paid', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_fees_mark_paid(fee_id):
|
||||
"""Mark a fee as paid"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
fee = db.query(MembershipFee).filter(MembershipFee.id == fee_id).first()
|
||||
@ -647,11 +618,9 @@ def admin_fees_mark_paid(fee_id):
|
||||
|
||||
@bp.route('/fees/bulk-mark-paid', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_fees_bulk_mark_paid():
|
||||
"""Bulk mark fees as paid"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
fee_ids = request.form.getlist('fee_ids[]', type=int)
|
||||
@ -686,12 +655,9 @@ def admin_fees_bulk_mark_paid():
|
||||
|
||||
@bp.route('/fees/export')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_fees_export():
|
||||
"""Export fees to CSV"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnien.', 'error')
|
||||
return redirect(url_for('.admin_fees'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
year = request.args.get('year', datetime.now().year, type=int)
|
||||
@ -747,12 +713,9 @@ def admin_fees_export():
|
||||
|
||||
@bp.route('/kalendarz')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_calendar():
|
||||
"""Panel admin - zarządzanie wydarzeniami"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień.', 'error')
|
||||
return redirect(url_for('calendar_index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).all()
|
||||
@ -764,12 +727,9 @@ def admin_calendar():
|
||||
|
||||
@bp.route('/kalendarz/nowy', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_calendar_new():
|
||||
"""Dodaj nowe wydarzenie"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień.', 'error')
|
||||
return redirect(url_for('calendar_index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -803,11 +763,9 @@ def admin_calendar_new():
|
||||
|
||||
@bp.route('/kalendarz/<int:event_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_calendar_delete(event_id):
|
||||
"""Usuń wydarzenie"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
event = db.query(NordaEvent).filter(NordaEvent.id == event_id).first()
|
||||
@ -834,14 +792,12 @@ def admin_calendar_delete(event_id):
|
||||
|
||||
@bp.route('/notify-release', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_notify_release():
|
||||
"""
|
||||
Send notifications to all users about a new release.
|
||||
Called manually by admin after deploying a new version.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
version = data.get('version')
|
||||
highlights = data.get('highlights', [])
|
||||
|
||||
@ -18,8 +18,10 @@ from sqlalchemy.orm import joinedload
|
||||
from . import bp
|
||||
from database import (
|
||||
SessionLocal, User, UserSession, PageView, SearchQuery,
|
||||
ConversionEvent, JSError, Company, AIUsageLog, AIUsageDaily
|
||||
ConversionEvent, JSError, Company, AIUsageLog, AIUsageDaily,
|
||||
SystemRole
|
||||
)
|
||||
from utils.decorators import role_required
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -30,12 +32,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/analytics')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_analytics():
|
||||
"""Admin dashboard for user analytics - sessions, page views, clicks"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnien do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
period = request.args.get('period', 'week')
|
||||
user_id = request.args.get('user_id', type=int)
|
||||
|
||||
@ -266,12 +265,9 @@ def admin_analytics():
|
||||
|
||||
@bp.route('/analytics/export')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_analytics_export():
|
||||
"""Export analytics data as CSV"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnien.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
export_type = request.args.get('type', 'sessions')
|
||||
period = request.args.get('period', 'month')
|
||||
|
||||
@ -373,12 +369,9 @@ def admin_analytics_export():
|
||||
|
||||
@bp.route('/ai-usage')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_ai_usage():
|
||||
"""Admin dashboard for AI (Gemini) API usage monitoring"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get period filter from query params
|
||||
@ -601,12 +594,9 @@ def admin_ai_usage():
|
||||
|
||||
@bp.route('/ai-usage/user/<int:user_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_ai_usage_user(user_id):
|
||||
"""Detailed AI usage for a specific user"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Get user info
|
||||
|
||||
@ -13,7 +13,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
from database import SessionLocal, Announcement
|
||||
from database import SessionLocal, Announcement, SystemRole
|
||||
from utils.decorators import role_required
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -48,12 +49,9 @@ def generate_slug(title):
|
||||
|
||||
@bp.route('/announcements')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements():
|
||||
"""Admin panel - lista ogłoszeń"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Filters
|
||||
@ -92,12 +90,9 @@ def admin_announcements():
|
||||
|
||||
@bp.route('/announcements/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements_new():
|
||||
"""Admin panel - nowe ogłoszenie"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -174,12 +169,9 @@ def admin_announcements_new():
|
||||
|
||||
@bp.route('/announcements/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements_edit(id):
|
||||
"""Admin panel - edycja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
@ -259,11 +251,9 @@ def admin_announcements_edit(id):
|
||||
|
||||
@bp.route('/announcements/<int:id>/publish', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements_publish(id):
|
||||
"""Publikacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
@ -300,11 +290,9 @@ def admin_announcements_publish(id):
|
||||
|
||||
@bp.route('/announcements/<int:id>/archive', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements_archive(id):
|
||||
"""Archiwizacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
@ -328,11 +316,9 @@ def admin_announcements_archive(id):
|
||||
|
||||
@bp.route('/announcements/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_announcements_delete(id):
|
||||
"""Usunięcie ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -15,7 +15,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify, R
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
from database import SessionLocal, Company, Category, User, Person, CompanyPerson
|
||||
from database import SessionLocal, Company, Category, User, Person, CompanyPerson, SystemRole
|
||||
from utils.decorators import role_required
|
||||
|
||||
# Logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -37,12 +38,9 @@ def validate_nip(nip: str) -> bool:
|
||||
|
||||
@bp.route('/companies')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_companies():
|
||||
"""Admin panel for company management"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Get filter parameters
|
||||
@ -104,11 +102,9 @@ def admin_companies():
|
||||
|
||||
@bp.route('/companies/add', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_add():
|
||||
"""Create a new company"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -174,11 +170,9 @@ def admin_company_add():
|
||||
|
||||
@bp.route('/companies/<int:company_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_get(company_id):
|
||||
"""Get company details (JSON)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -207,11 +201,9 @@ def admin_company_get(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_update(company_id):
|
||||
"""Update company data"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -278,11 +270,9 @@ def admin_company_update(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/toggle-status', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_toggle_status(company_id):
|
||||
"""Toggle company status (active <-> inactive)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -310,11 +300,9 @@ def admin_company_toggle_status(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_delete(company_id):
|
||||
"""Soft delete company (set status to archived)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -337,11 +325,9 @@ def admin_company_delete(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/assign-user', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_assign_user(company_id):
|
||||
"""Assign a user to a company"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -373,11 +359,9 @@ def admin_company_assign_user(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/people')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_people(company_id):
|
||||
"""Get people associated with a company"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -414,11 +398,9 @@ def admin_company_people(company_id):
|
||||
|
||||
@bp.route('/companies/<int:company_id>/users')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_users(company_id):
|
||||
"""Get users assigned to a company"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
@ -446,12 +428,9 @@ def admin_company_users(company_id):
|
||||
|
||||
@bp.route('/companies/export')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_companies_export():
|
||||
"""Export companies to CSV"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
companies = db.query(Company).order_by(Company.name).all()
|
||||
|
||||
@ -10,6 +10,8 @@ import logging
|
||||
from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from database import SystemRole
|
||||
from utils.decorators import role_required
|
||||
from . import bp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -21,22 +23,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/insights')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_insights():
|
||||
"""Admin dashboard for development insights from forum and chat"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template('admin/insights.html')
|
||||
|
||||
|
||||
@bp.route('/insights-api', methods=['GET'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_get_insights():
|
||||
"""Get development insights for roadmap"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
try:
|
||||
from norda_knowledge_service import get_knowledge_service
|
||||
service = get_knowledge_service()
|
||||
@ -61,11 +58,9 @@ def api_get_insights():
|
||||
|
||||
@bp.route('/insights-api/<int:insight_id>/status', methods=['PUT'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_update_insight_status(insight_id):
|
||||
"""Update insight status (for roadmap planning)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
try:
|
||||
from norda_knowledge_service import get_knowledge_service
|
||||
service = get_knowledge_service()
|
||||
@ -87,11 +82,9 @@ def api_update_insight_status(insight_id):
|
||||
|
||||
@bp.route('/insights-api/sync', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_sync_insights():
|
||||
"""Manually trigger knowledge sync from forum and chat"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
try:
|
||||
from norda_knowledge_service import get_knowledge_service
|
||||
service = get_knowledge_service()
|
||||
@ -121,11 +114,9 @@ def api_sync_insights():
|
||||
|
||||
@bp.route('/insights-api/stats', methods=['GET'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_insights_stats():
|
||||
"""Get knowledge base statistics"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
try:
|
||||
from norda_knowledge_service import get_knowledge_service
|
||||
service = get_knowledge_service()
|
||||
@ -152,11 +143,9 @@ def api_insights_stats():
|
||||
|
||||
@bp.route('/ai-learning-status')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_ai_learning_status():
|
||||
"""API: Get AI feedback learning status and examples"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
try:
|
||||
from feedback_learning_service import get_feedback_learning_service
|
||||
service = get_feedback_learning_service()
|
||||
@ -211,11 +200,9 @@ def api_ai_learning_status():
|
||||
|
||||
@bp.route('/chat-stats')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_chat_stats():
|
||||
"""API: Get chat statistics for dashboard"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from database import SessionLocal, AIChatMessage
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -16,9 +16,11 @@ from sqlalchemy.orm.attributes import flag_modified
|
||||
from . import bp
|
||||
from database import (
|
||||
SessionLocal, MembershipApplication, CompanyDataRequest,
|
||||
Company, Category, User, UserNotification, Person, CompanyPerson, CompanyPKD
|
||||
Company, Category, User, UserNotification, Person, CompanyPerson, CompanyPKD,
|
||||
SystemRole
|
||||
)
|
||||
from krs_api_service import get_company_from_krs
|
||||
from utils.decorators import role_required
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -156,12 +158,9 @@ def _enrich_company_from_krs(company, db):
|
||||
|
||||
@bp.route('/membership')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership():
|
||||
"""Admin panel for membership applications."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Get filter parameters
|
||||
@ -224,12 +223,9 @@ def admin_membership():
|
||||
|
||||
@bp.route('/membership/<int:app_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_detail(app_id):
|
||||
"""View membership application details."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -253,11 +249,9 @@ def admin_membership_detail(app_id):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/approve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_approve(app_id):
|
||||
"""Approve membership application and create company."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -398,11 +392,9 @@ def admin_membership_approve(app_id):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/reject', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_reject(app_id):
|
||||
"""Reject membership application."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -444,11 +436,9 @@ def admin_membership_reject(app_id):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/request-changes', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_request_changes(app_id):
|
||||
"""Request changes to membership application."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -490,11 +480,9 @@ def admin_membership_request_changes(app_id):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/start-review', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_start_review(app_id):
|
||||
"""Mark application as under review."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -521,14 +509,12 @@ def admin_membership_start_review(app_id):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/propose-changes', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_propose_changes(app_id):
|
||||
"""
|
||||
Propose changes from registry data for user approval.
|
||||
Instead of directly updating, save proposed changes and notify user.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
@ -651,15 +637,13 @@ def _get_field_label(field_name):
|
||||
|
||||
@bp.route('/membership/<int:app_id>/update-from-registry', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_membership_update_from_registry(app_id):
|
||||
"""
|
||||
[DEPRECATED - use propose-changes instead]
|
||||
Direct update is now replaced by propose-changes workflow.
|
||||
This endpoint is kept for backward compatibility but redirects to propose-changes.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Redirect to new workflow
|
||||
data = request.get_json() or {}
|
||||
return admin_membership_propose_changes.__wrapped__(app_id)
|
||||
@ -671,12 +655,9 @@ def admin_membership_update_from_registry(app_id):
|
||||
|
||||
@bp.route('/company-requests')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_requests():
|
||||
"""Admin panel for company data requests."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
status_filter = request.args.get('status', 'pending')
|
||||
@ -713,11 +694,9 @@ def admin_company_requests():
|
||||
|
||||
@bp.route('/company-requests/<int:req_id>/approve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_request_approve(req_id):
|
||||
"""Approve company data request and update company."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data_request = db.query(CompanyDataRequest).get(req_id)
|
||||
@ -803,11 +782,9 @@ def admin_company_request_approve(req_id):
|
||||
|
||||
@bp.route('/company-requests/<int:req_id>/reject', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_request_reject(req_id):
|
||||
"""Reject company data request."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data_request = db.query(CompanyDataRequest).get(req_id)
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -13,7 +13,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
from database import SessionLocal, Company, Person, CompanyPerson
|
||||
from database import SessionLocal, Company, Person, CompanyPerson, SystemRole
|
||||
from utils.decorators import role_required
|
||||
|
||||
# Logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -25,12 +26,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/people')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_people():
|
||||
"""Admin panel for person management"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Get search query
|
||||
@ -114,11 +112,9 @@ def admin_people():
|
||||
|
||||
@bp.route('/people/add', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_add():
|
||||
"""Create a new person"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -171,11 +167,9 @@ def admin_person_add():
|
||||
|
||||
@bp.route('/people/<int:person_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_get(person_id):
|
||||
"""Get person details (JSON)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
@ -198,11 +192,9 @@ def admin_person_get(person_id):
|
||||
|
||||
@bp.route('/people/<int:person_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_update(person_id):
|
||||
"""Update person data"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
@ -256,11 +248,9 @@ def admin_person_update(person_id):
|
||||
|
||||
@bp.route('/people/<int:person_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_delete(person_id):
|
||||
"""Delete person (hard delete with CASCADE on CompanyPerson)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
@ -286,11 +276,9 @@ def admin_person_delete(person_id):
|
||||
|
||||
@bp.route('/people/<int:person_id>/companies')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_companies(person_id):
|
||||
"""Get companies associated with a person"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
@ -321,11 +309,9 @@ def admin_person_companies(person_id):
|
||||
|
||||
@bp.route('/people/<int:person_id>/link-company', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_link_company(person_id):
|
||||
"""Link person to a company"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -393,11 +379,9 @@ def admin_person_link_company(person_id):
|
||||
|
||||
@bp.route('/people/<int:person_id>/unlink-company/<int:company_id>', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_person_unlink_company(person_id, company_id):
|
||||
"""Remove person-company link"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@ -439,11 +423,9 @@ def admin_person_unlink_company(person_id, company_id):
|
||||
|
||||
@bp.route('/people/search')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_people_search():
|
||||
"""Search people for autocomplete"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = request.args.get('q', '').strip()
|
||||
|
||||
@ -12,7 +12,8 @@ from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
from database import SessionLocal, User, AuditLog, SecurityAlert
|
||||
from database import SessionLocal, User, AuditLog, SecurityAlert, SystemRole
|
||||
from utils.decorators import role_required
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -34,12 +35,9 @@ except ImportError:
|
||||
|
||||
@bp.route('/security')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_security():
|
||||
"""Security dashboard - audit logs, alerts, GeoIP stats"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from sqlalchemy import func, desc
|
||||
@ -158,11 +156,9 @@ def admin_security():
|
||||
|
||||
@bp.route('/security/alert/<int:alert_id>/acknowledge', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def acknowledge_security_alert(alert_id):
|
||||
"""Acknowledge a security alert"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
alert = db.query(SecurityAlert).get(alert_id)
|
||||
@ -186,11 +182,9 @@ def acknowledge_security_alert(alert_id):
|
||||
|
||||
@bp.route('/security/alert/<int:alert_id>/resolve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def resolve_security_alert(alert_id):
|
||||
"""Resolve a security alert"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
note = request.form.get('note', '')
|
||||
|
||||
db = SessionLocal()
|
||||
@ -219,11 +213,9 @@ def resolve_security_alert(alert_id):
|
||||
|
||||
@bp.route('/security/unlock-account/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def unlock_account(user_id):
|
||||
"""Unlock a locked user account"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).get(user_id)
|
||||
@ -246,11 +238,9 @@ def unlock_account(user_id):
|
||||
|
||||
@bp.route('/security/geoip-stats')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_geoip_stats():
|
||||
"""API endpoint for GeoIP stats auto-refresh"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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'},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,11 +18,13 @@ from sqlalchemy import text, func, distinct
|
||||
|
||||
from database import (
|
||||
SessionLocal,
|
||||
SystemRole,
|
||||
ZOPKNews,
|
||||
ZOPKKnowledgeChunk,
|
||||
ZOPKKnowledgeEntity,
|
||||
ZOPKKnowledgeEntityMention
|
||||
)
|
||||
from utils.decorators import role_required
|
||||
from . import bp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -35,6 +37,7 @@ _GRAPH_CACHE_TTL = 300 # 5 minutes
|
||||
|
||||
@bp.route('/zopk/knowledge/stats')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_stats():
|
||||
"""
|
||||
Get knowledge extraction statistics.
|
||||
@ -44,9 +47,6 @@ def admin_zopk_knowledge_stats():
|
||||
- knowledge_base: stats about chunks, facts, entities, relations
|
||||
- top_entities: most mentioned entities
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_knowledge_stats
|
||||
|
||||
db = SessionLocal()
|
||||
@ -65,6 +65,7 @@ def admin_zopk_knowledge_stats():
|
||||
|
||||
@bp.route('/zopk/knowledge/extract', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_extract():
|
||||
"""
|
||||
Batch extract knowledge from scraped articles.
|
||||
@ -77,9 +78,6 @@ def admin_zopk_knowledge_extract():
|
||||
- chunks/facts/entities/relations created
|
||||
- errors list
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import ZOPKKnowledgeService
|
||||
|
||||
db = SessionLocal()
|
||||
@ -114,13 +112,11 @@ def admin_zopk_knowledge_extract():
|
||||
|
||||
@bp.route('/zopk/knowledge/extract/<int:news_id>', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_extract_single(news_id):
|
||||
"""
|
||||
Extract knowledge from a single article.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import ZOPKKnowledgeService
|
||||
|
||||
db = SessionLocal()
|
||||
@ -155,6 +151,7 @@ def admin_zopk_knowledge_extract_single(news_id):
|
||||
|
||||
@bp.route('/zopk/knowledge/embeddings', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_generate_embeddings():
|
||||
"""
|
||||
Generate embeddings for chunks that don't have them.
|
||||
@ -162,9 +159,6 @@ def admin_zopk_generate_embeddings():
|
||||
Request JSON:
|
||||
- limit: int (default 100) - max chunks to process
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import generate_chunk_embeddings
|
||||
|
||||
db = SessionLocal()
|
||||
@ -192,6 +186,7 @@ def admin_zopk_generate_embeddings():
|
||||
|
||||
@bp.route('/zopk/knowledge/extract/stream', methods=['GET'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_extract_stream():
|
||||
"""
|
||||
SSE endpoint for streaming knowledge extraction progress.
|
||||
@ -199,9 +194,6 @@ def admin_zopk_knowledge_extract_stream():
|
||||
Query params:
|
||||
- limit: int (default 10) - max articles to process
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
limit = min(int(request.args.get('limit', 10)), 50)
|
||||
user_id = current_user.id
|
||||
|
||||
@ -275,6 +267,7 @@ def admin_zopk_knowledge_extract_stream():
|
||||
|
||||
@bp.route('/zopk/knowledge/embeddings/stream', methods=['GET'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_embeddings_stream():
|
||||
"""
|
||||
SSE endpoint for streaming embeddings generation progress.
|
||||
@ -282,9 +275,6 @@ def admin_zopk_embeddings_stream():
|
||||
Query params:
|
||||
- limit: int (default 50) - max chunks to process
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
limit = min(int(request.args.get('limit', 50)), 200)
|
||||
user_id = current_user.id
|
||||
|
||||
@ -413,28 +403,22 @@ def api_zopk_knowledge_search():
|
||||
|
||||
@bp.route('/zopk/knowledge')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_dashboard():
|
||||
"""
|
||||
Dashboard for ZOPK Knowledge Base management.
|
||||
Shows stats and links to chunks, facts, entities lists.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('admin/zopk_knowledge_dashboard.html')
|
||||
|
||||
|
||||
@bp.route('/zopk/knowledge/chunks')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_chunks():
|
||||
"""
|
||||
List knowledge chunks with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_chunks
|
||||
|
||||
# Get query params
|
||||
@ -478,14 +462,11 @@ def admin_zopk_knowledge_chunks():
|
||||
|
||||
@bp.route('/zopk/knowledge/facts')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_facts():
|
||||
"""
|
||||
List knowledge facts with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_facts
|
||||
|
||||
# Get query params
|
||||
@ -528,14 +509,11 @@ def admin_zopk_knowledge_facts():
|
||||
|
||||
@bp.route('/zopk/knowledge/entities')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_entities():
|
||||
"""
|
||||
List knowledge entities with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_entities
|
||||
|
||||
# Get query params
|
||||
@ -578,11 +556,9 @@ def admin_zopk_knowledge_entities():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_chunk_detail(chunk_id):
|
||||
"""Get detailed information about a chunk."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_chunk_detail
|
||||
|
||||
db = SessionLocal()
|
||||
@ -598,11 +574,9 @@ def api_zopk_chunk_detail(chunk_id):
|
||||
|
||||
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_chunk_verify(chunk_id):
|
||||
"""Toggle chunk verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_chunk_verification
|
||||
|
||||
db = SessionLocal()
|
||||
@ -624,11 +598,9 @@ def api_zopk_chunk_verify(chunk_id):
|
||||
|
||||
@bp.route('/zopk-api/knowledge/facts/<int:fact_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_fact_verify(fact_id):
|
||||
"""Toggle fact verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_fact_verification
|
||||
|
||||
db = SessionLocal()
|
||||
@ -650,11 +622,9 @@ def api_zopk_fact_verify(fact_id):
|
||||
|
||||
@bp.route('/zopk-api/knowledge/entities/<int:entity_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_entity_verify(entity_id):
|
||||
"""Toggle entity verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_entity_verification
|
||||
|
||||
db = SessionLocal()
|
||||
@ -676,11 +646,9 @@ def api_zopk_entity_verify(entity_id):
|
||||
|
||||
@bp.route('/zopk-api/knowledge/chunks/<int:chunk_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_chunk_delete(chunk_id):
|
||||
"""Delete a chunk and its associated data."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import delete_chunk
|
||||
|
||||
db = SessionLocal()
|
||||
@ -699,12 +667,9 @@ def api_zopk_chunk_delete(chunk_id):
|
||||
|
||||
@bp.route('/zopk/knowledge/duplicates')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_duplicates():
|
||||
"""Admin page for managing duplicate entities."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
from zopk_knowledge_service import find_duplicate_entities
|
||||
|
||||
db = SessionLocal()
|
||||
@ -737,11 +702,9 @@ def admin_zopk_knowledge_duplicates():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/duplicates/preview', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_duplicates_preview():
|
||||
"""Preview merge operation between two entities."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_entity_merge_preview
|
||||
|
||||
db = SessionLocal()
|
||||
@ -764,11 +727,9 @@ def api_zopk_duplicates_preview():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/duplicates/merge', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_duplicates_merge():
|
||||
"""Merge two entities - keep primary, delete duplicate."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import merge_entities
|
||||
|
||||
db = SessionLocal()
|
||||
@ -789,17 +750,15 @@ def api_zopk_duplicates_merge():
|
||||
|
||||
@bp.route('/zopk/knowledge/graph')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_knowledge_graph():
|
||||
"""Admin page for entity relations graph visualization."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template('admin/zopk_knowledge_graph.html')
|
||||
|
||||
|
||||
@bp.route('/zopk-api/knowledge/graph/data')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_knowledge_graph_data():
|
||||
"""Get graph data for entity co-occurrence visualization.
|
||||
|
||||
@ -808,9 +767,6 @@ def api_zopk_knowledge_graph_data():
|
||||
"""
|
||||
global _graph_cache
|
||||
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Build cache key from parameters
|
||||
entity_type = request.args.get('entity_type', '')
|
||||
min_cooccurrence = int(request.args.get('min_cooccurrence', 3))
|
||||
@ -923,21 +879,17 @@ def api_zopk_knowledge_graph_data():
|
||||
|
||||
@bp.route('/zopk/knowledge/fact-duplicates')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_fact_duplicates():
|
||||
"""Panel deduplikacji faktów."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('admin/zopk_fact_duplicates.html')
|
||||
|
||||
|
||||
@bp.route('/zopk-api/knowledge/fact-duplicates')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_fact_duplicates():
|
||||
"""API - lista duplikatów faktów."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import find_duplicate_facts
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -955,11 +907,9 @@ def api_zopk_fact_duplicates():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/fact-duplicates/merge', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_fact_merge():
|
||||
"""API - merge duplikatów faktów."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import merge_facts
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -978,11 +928,9 @@ def api_zopk_fact_merge():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/auto-verify/entities', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_auto_verify_entities():
|
||||
"""Auto-weryfikacja encji z wysoką liczbą wzmianek."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import auto_verify_top_entities
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -1000,11 +948,9 @@ def api_zopk_auto_verify_entities():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/auto-verify/facts', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_auto_verify_facts():
|
||||
"""Auto-weryfikacja faktów z wysoką ważnością."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import auto_verify_top_facts
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -1022,11 +968,9 @@ def api_zopk_auto_verify_facts():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/auto-verify/similar', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_auto_verify_similar():
|
||||
"""Auto-weryfikacja faktów podobnych do już zweryfikowanych (uczenie się)."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import auto_verify_similar_to_verified
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -1044,11 +988,9 @@ def api_zopk_auto_verify_similar():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/suggest-similar-facts')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_suggest_similar_facts():
|
||||
"""Pobierz sugestie faktów podobnych do zweryfikowanych (bez auto-weryfikacji)."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import find_similar_to_verified_facts
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -1069,11 +1011,9 @@ def api_zopk_suggest_similar_facts():
|
||||
|
||||
@bp.route('/zopk-api/knowledge/dashboard-stats')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_dashboard_stats():
|
||||
"""API - statystyki dashboardu."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_knowledge_dashboard_stats
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
||||
@ -18,10 +18,12 @@ from sqlalchemy.sql import nullslast
|
||||
|
||||
from database import (
|
||||
SessionLocal,
|
||||
SystemRole,
|
||||
ZOPKProject,
|
||||
ZOPKNews,
|
||||
ZOPKNewsFetchJob
|
||||
)
|
||||
from utils.decorators import role_required
|
||||
from . import bp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -29,11 +31,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/zopk/news')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news():
|
||||
"""Admin news management for ZOPK"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -90,10 +90,9 @@ def admin_zopk_news():
|
||||
|
||||
@bp.route('/zopk/news/<int:news_id>/approve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news_approve(news_id):
|
||||
"""Approve a ZOPK news item"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -119,10 +118,9 @@ def admin_zopk_news_approve(news_id):
|
||||
|
||||
@bp.route('/zopk/news/<int:news_id>/reject', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news_reject(news_id):
|
||||
"""Reject a ZOPK news item"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -152,10 +150,9 @@ def admin_zopk_news_reject(news_id):
|
||||
|
||||
@bp.route('/zopk/news/add', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news_add():
|
||||
"""Manually add a ZOPK news item"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -233,10 +230,9 @@ def admin_zopk_news_add():
|
||||
|
||||
@bp.route('/zopk/news/reject-old', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_reject_old_news():
|
||||
"""Reject all news from before a certain year (ZOPK didn't exist then)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -278,10 +274,9 @@ def admin_zopk_reject_old_news():
|
||||
|
||||
@bp.route('/zopk/news/star-counts')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news_star_counts():
|
||||
"""Get counts of pending news items grouped by star rating"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -318,10 +313,9 @@ def admin_zopk_news_star_counts():
|
||||
|
||||
@bp.route('/zopk/news/reject-by-stars', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_reject_by_stars():
|
||||
"""Reject all pending news items with specified star ratings"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -383,10 +377,9 @@ def admin_zopk_reject_by_stars():
|
||||
|
||||
@bp.route('/zopk/news/evaluate-ai', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_evaluate_ai():
|
||||
"""Evaluate pending news for ZOPK relevance using Gemini AI"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_news_service import evaluate_pending_news
|
||||
|
||||
@ -418,10 +411,9 @@ def admin_zopk_evaluate_ai():
|
||||
|
||||
@bp.route('/zopk/news/reevaluate-scores', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_reevaluate_scores():
|
||||
"""Re-evaluate news items that have ai_relevant but no ai_relevance_score (1-5 stars)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_news_service import reevaluate_news_without_score
|
||||
|
||||
@ -453,6 +445,7 @@ def admin_zopk_reevaluate_scores():
|
||||
|
||||
@bp.route('/zopk/news/reevaluate-low-scores', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_reevaluate_low_scores():
|
||||
"""
|
||||
Re-evaluate news with low AI scores (1-2★) that contain key ZOPK topics.
|
||||
@ -461,9 +454,6 @@ def admin_zopk_reevaluate_low_scores():
|
||||
Old articles scored low before these topics were recognized will be re-evaluated
|
||||
and potentially upgraded.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_news_service import reevaluate_low_score_news
|
||||
|
||||
db = SessionLocal()
|
||||
@ -496,6 +486,7 @@ def admin_zopk_reevaluate_low_scores():
|
||||
|
||||
@bp.route('/zopk-api/search-news', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_search_news():
|
||||
"""
|
||||
Search for ZOPK news using multiple sources with cross-verification.
|
||||
@ -509,9 +500,6 @@ def api_zopk_search_news():
|
||||
- 1 source → pending (manual review)
|
||||
- 3+ sources → auto_approved
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_news_service import ZOPKNewsService
|
||||
|
||||
db = SessionLocal()
|
||||
@ -594,6 +582,7 @@ def api_zopk_search_news():
|
||||
|
||||
@bp.route('/zopk/news/scrape-stats')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_scrape_stats():
|
||||
"""
|
||||
Get content scraping statistics.
|
||||
@ -606,9 +595,6 @@ def admin_zopk_scrape_stats():
|
||||
- skipped: Skipped (social media, paywalls)
|
||||
- ready_for_extraction: Scraped but not yet processed for knowledge
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_content_scraper import get_scrape_stats
|
||||
|
||||
db = SessionLocal()
|
||||
@ -627,6 +613,7 @@ def admin_zopk_scrape_stats():
|
||||
|
||||
@bp.route('/zopk/news/scrape-content', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_scrape_content():
|
||||
"""
|
||||
Batch scrape article content from source URLs.
|
||||
@ -642,9 +629,6 @@ def admin_zopk_scrape_content():
|
||||
- errors: list of error details
|
||||
- scraped_articles: list of scraped article info
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_content_scraper import ZOPKContentScraper
|
||||
|
||||
db = SessionLocal()
|
||||
@ -673,13 +657,11 @@ def admin_zopk_scrape_content():
|
||||
|
||||
@bp.route('/zopk/news/<int:news_id>/scrape', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_scrape_single(news_id):
|
||||
"""
|
||||
Scrape content for a single article.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_content_scraper import ZOPKContentScraper
|
||||
|
||||
db = SessionLocal()
|
||||
@ -711,6 +693,7 @@ def admin_zopk_scrape_single(news_id):
|
||||
|
||||
@bp.route('/zopk/news/scrape-content/stream', methods=['GET'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_news_scrape_stream():
|
||||
"""
|
||||
Stream scraping progress using Server-Sent Events.
|
||||
@ -719,9 +702,6 @@ def admin_zopk_news_scrape_stream():
|
||||
- limit: int (default 50)
|
||||
- force: bool (default false)
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_content_scraper import ZOPKContentScraper, MAX_RETRY_ATTEMPTS
|
||||
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
|
||||
@ -13,8 +13,10 @@ from flask_login import current_user, login_required
|
||||
|
||||
from database import (
|
||||
SessionLocal,
|
||||
SystemRole,
|
||||
ZOPKMilestone
|
||||
)
|
||||
from utils.decorators import role_required
|
||||
from . import bp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -22,11 +24,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/zopk/timeline')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_zopk_timeline():
|
||||
"""Panel Timeline ZOPK."""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('admin/zopk_timeline.html')
|
||||
|
||||
|
||||
@ -58,10 +58,9 @@ def api_zopk_milestones():
|
||||
|
||||
@bp.route('/zopk-api/milestones', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_milestone_create():
|
||||
"""API - utworzenie kamienia milowego."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -88,10 +87,9 @@ def api_zopk_milestone_create():
|
||||
|
||||
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_milestone_update(milestone_id):
|
||||
"""API - aktualizacja kamienia milowego."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -126,10 +124,9 @@ def api_zopk_milestone_update(milestone_id):
|
||||
|
||||
@bp.route('/zopk-api/milestones/<int:milestone_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_milestone_delete(milestone_id):
|
||||
"""API - usunięcie kamienia milowego."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -149,10 +146,9 @@ def api_zopk_milestone_delete(milestone_id):
|
||||
|
||||
@bp.route('/zopk-api/timeline/suggestions')
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_timeline_suggestions():
|
||||
"""API - sugestie kamieni milowych z bazy wiedzy."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_timeline_suggestions
|
||||
|
||||
@ -177,10 +173,9 @@ def api_zopk_timeline_suggestions():
|
||||
|
||||
@bp.route('/zopk-api/timeline/suggestions/approve', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def api_zopk_timeline_suggestion_approve():
|
||||
"""API - zatwierdzenie sugestii i utworzenie kamienia milowego."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
from zopk_knowledge_service import create_milestone_from_suggestion
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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})")
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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.'
|
||||
|
||||
@ -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})")
|
||||
|
||||
|
||||
11
blueprints/audit/CLAUDE.md
Normal file
11
blueprints/audit/CLAUDE.md
Normal file
@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 31, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #175 | 6:24 PM | 🔵 | Nordabiz audit blueprint provides user-facing dashboards for SEO, GBP, social media, and IT audits | ~542 |
|
||||
</claude-mem-context>
|
||||
@ -66,11 +66,10 @@ def seo_audit_dashboard(slug):
|
||||
flash('Firma nie została znaleziona.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Access control: admin can view any company, member only their own
|
||||
if not current_user.is_admin:
|
||||
if current_user.company_id != company.id:
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
# Access control: users with company edit rights can view
|
||||
if not current_user.can_edit_company(company.id):
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Get latest SEO analysis for this company
|
||||
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
||||
@ -90,8 +89,8 @@ def seo_audit_dashboard(slug):
|
||||
'url': analysis.website_url
|
||||
}
|
||||
|
||||
# Determine if user can run audit (admin or company owner)
|
||||
can_audit = current_user.is_admin or current_user.company_id == company.id
|
||||
# Determine if user can run audit (user with company edit rights)
|
||||
can_audit = current_user.can_edit_company(company.id)
|
||||
|
||||
logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}")
|
||||
|
||||
@ -139,11 +138,10 @@ def social_audit_dashboard(slug):
|
||||
flash('Firma nie została znaleziona.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Access control - admin can view all, users only their company
|
||||
if not current_user.is_admin:
|
||||
if current_user.company_id != company.id:
|
||||
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
# Access control - users with company edit rights can view
|
||||
if not current_user.can_edit_company(company.id):
|
||||
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Get social media profiles for this company
|
||||
social_profiles = db.query(CompanySocialMedia).filter(
|
||||
@ -179,8 +177,8 @@ def social_audit_dashboard(slug):
|
||||
'score': score
|
||||
}
|
||||
|
||||
# Determine if user can run audit (admin or company owner)
|
||||
can_audit = current_user.is_admin or current_user.company_id == company.id
|
||||
# Determine if user can run audit (user with company edit rights)
|
||||
can_audit = current_user.can_edit_company(company.id)
|
||||
|
||||
logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}")
|
||||
|
||||
@ -233,11 +231,10 @@ def gbp_audit_dashboard(slug):
|
||||
flash('Firma nie została znaleziona.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Access control: admin can view any company, member only their own
|
||||
if not current_user.is_admin:
|
||||
if current_user.company_id != company.id:
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
# Access control: users with company edit rights can view
|
||||
if not current_user.can_edit_company(company.id):
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Get latest audit for this company
|
||||
audit = gbp_get_company_audit(db, company.id)
|
||||
@ -245,8 +242,8 @@ def gbp_audit_dashboard(slug):
|
||||
# If no audit exists, we still render the page (template handles this)
|
||||
# The user can trigger an audit from the dashboard
|
||||
|
||||
# Determine if user can run audit (admin or company owner)
|
||||
can_audit = current_user.is_admin or current_user.company_id == company.id
|
||||
# Determine if user can run audit (user with company edit rights)
|
||||
can_audit = current_user.can_edit_company(company.id)
|
||||
|
||||
logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}")
|
||||
|
||||
@ -297,11 +294,10 @@ def it_audit_dashboard(slug):
|
||||
flash('Firma nie została znaleziona.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Access control: admin can view any company, member only their own
|
||||
if not current_user.is_admin:
|
||||
if current_user.company_id != company.id:
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
# Access control: users with company edit rights can view
|
||||
if not current_user.can_edit_company(company.id):
|
||||
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Get latest IT audit for this company
|
||||
audit = db.query(ITAudit).filter(
|
||||
@ -356,8 +352,8 @@ def it_audit_dashboard(slug):
|
||||
'recommendations': audit.recommendations
|
||||
}
|
||||
|
||||
# Determine if user can edit audit (admin or company owner)
|
||||
can_edit = current_user.is_admin or current_user.company_id == company.id
|
||||
# Determine if user can edit audit (user with company edit rights)
|
||||
can_edit = current_user.can_edit_company(company.id)
|
||||
|
||||
logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}")
|
||||
|
||||
|
||||
11
blueprints/chat/CLAUDE.md
Normal file
11
blueprints/chat/CLAUDE.md
Normal file
@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 31, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #180 | 6:25 PM | 🔵 | Nordabiz project architecture analyzed revealing 16+ Flask blueprints with modular organization | ~831 |
|
||||
</claude-mem-context>
|
||||
@ -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'))
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -221,8 +221,8 @@ def forum_topic(topic_id):
|
||||
flash('Temat nie istnieje.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
# Check if topic is soft-deleted (only admins can view)
|
||||
if topic.is_deleted and not current_user.is_admin:
|
||||
# Check if topic is soft-deleted (only moderators can view)
|
||||
if topic.is_deleted and not current_user.can_moderate_forum():
|
||||
flash('Temat nie istnieje.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
@ -237,9 +237,9 @@ def forum_topic(topic_id):
|
||||
if not existing_topic_read:
|
||||
db.add(ForumTopicRead(topic_id=topic.id, user_id=current_user.id))
|
||||
|
||||
# Filter soft-deleted replies for non-admins
|
||||
# Filter soft-deleted replies for non-moderators
|
||||
visible_replies = [r for r in topic.replies
|
||||
if not r.is_deleted or current_user.is_admin]
|
||||
if not r.is_deleted or current_user.can_moderate_forum()]
|
||||
|
||||
# Record read for all visible replies
|
||||
for reply in visible_replies:
|
||||
@ -397,7 +397,7 @@ def forum_reply(topic_id):
|
||||
@login_required
|
||||
def admin_forum():
|
||||
"""Admin panel for forum moderation"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
@ -451,7 +451,7 @@ def admin_forum():
|
||||
@login_required
|
||||
def admin_forum_pin(topic_id):
|
||||
"""Toggle topic pin status"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -477,7 +477,7 @@ def admin_forum_pin(topic_id):
|
||||
@login_required
|
||||
def admin_forum_lock(topic_id):
|
||||
"""Toggle topic lock status"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -503,7 +503,7 @@ def admin_forum_lock(topic_id):
|
||||
@login_required
|
||||
def admin_forum_delete_topic(topic_id):
|
||||
"""Delete topic and all its replies"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -529,7 +529,7 @@ def admin_forum_delete_topic(topic_id):
|
||||
@login_required
|
||||
def admin_forum_delete_reply(reply_id):
|
||||
"""Delete a reply"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -554,8 +554,8 @@ def admin_forum_delete_reply(reply_id):
|
||||
@bp.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST'])
|
||||
@login_required
|
||||
def admin_forum_change_status(topic_id):
|
||||
"""Change topic status (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
"""Change topic status (moderators only)"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -593,8 +593,8 @@ def admin_forum_change_status(topic_id):
|
||||
@bp.route('/admin/forum/bulk-action', methods=['POST'])
|
||||
@login_required
|
||||
def admin_forum_bulk_action():
|
||||
"""Perform bulk action on multiple topics (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
"""Perform bulk action on multiple topics (moderators only)"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -700,12 +700,12 @@ def edit_topic(topic_id):
|
||||
if not topic:
|
||||
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
|
||||
|
||||
# Check ownership (unless admin)
|
||||
if topic.author_id != current_user.id and not current_user.is_admin:
|
||||
# Check ownership (unless moderator)
|
||||
if topic.author_id != current_user.id and not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Check time limit (unless admin)
|
||||
if not current_user.is_admin and not _can_edit_content(topic.created_at):
|
||||
# Check time limit (unless moderator)
|
||||
if not current_user.can_moderate_forum() and not _can_edit_content(topic.created_at):
|
||||
return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403
|
||||
|
||||
if topic.is_locked:
|
||||
@ -756,12 +756,12 @@ def edit_reply(reply_id):
|
||||
if not reply:
|
||||
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
|
||||
|
||||
# Check ownership (unless admin)
|
||||
if reply.author_id != current_user.id and not current_user.is_admin:
|
||||
# Check ownership (unless moderator)
|
||||
if reply.author_id != current_user.id and not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Check time limit (unless admin)
|
||||
if not current_user.is_admin and not _can_edit_content(reply.created_at):
|
||||
# Check time limit (unless moderator)
|
||||
if not current_user.can_moderate_forum() and not _can_edit_content(reply.created_at):
|
||||
return jsonify({'success': False, 'error': 'Minął limit czasu edycji (24h)'}), 403
|
||||
|
||||
# Check if topic is locked
|
||||
@ -809,7 +809,7 @@ def delete_own_reply(reply_id):
|
||||
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
|
||||
|
||||
# Check ownership
|
||||
if reply.author_id != current_user.id and not current_user.is_admin:
|
||||
if reply.author_id != current_user.id and not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Check if topic is locked
|
||||
@ -1094,8 +1094,8 @@ def report_content():
|
||||
@bp.route('/admin/forum/topic/<int:topic_id>/admin-edit', methods=['POST'])
|
||||
@login_required
|
||||
def admin_edit_topic(topic_id):
|
||||
"""Admin: Edit any topic content"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Edit any topic content"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -1150,8 +1150,8 @@ def admin_edit_topic(topic_id):
|
||||
@bp.route('/admin/forum/reply/<int:reply_id>/admin-edit', methods=['POST'])
|
||||
@login_required
|
||||
def admin_edit_reply(reply_id):
|
||||
"""Admin: Edit any reply content"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Edit any reply content"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -1196,8 +1196,8 @@ def admin_edit_reply(reply_id):
|
||||
@bp.route('/admin/forum/reply/<int:reply_id>/solution', methods=['POST'])
|
||||
@login_required
|
||||
def mark_as_solution(reply_id):
|
||||
"""Admin: Mark reply as solution"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Mark reply as solution"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -1251,8 +1251,8 @@ def mark_as_solution(reply_id):
|
||||
@bp.route('/admin/forum/topic/<int:topic_id>/restore', methods=['POST'])
|
||||
@login_required
|
||||
def restore_topic(topic_id):
|
||||
"""Admin: Restore soft-deleted topic"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Restore soft-deleted topic"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -1281,8 +1281,8 @@ def restore_topic(topic_id):
|
||||
@bp.route('/admin/forum/reply/<int:reply_id>/restore', methods=['POST'])
|
||||
@login_required
|
||||
def restore_reply(reply_id):
|
||||
"""Admin: Restore soft-deleted reply"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Restore soft-deleted reply"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -1311,8 +1311,8 @@ def restore_reply(reply_id):
|
||||
@bp.route('/admin/forum/reports')
|
||||
@login_required
|
||||
def admin_forum_reports():
|
||||
"""Admin: View all reports"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: View all reports"""
|
||||
if not current_user.can_moderate_forum():
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
@ -1348,8 +1348,8 @@ def admin_forum_reports():
|
||||
@bp.route('/admin/forum/report/<int:report_id>/review', methods=['POST'])
|
||||
@login_required
|
||||
def review_report(report_id):
|
||||
"""Admin: Review a report"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: Review a report"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -1383,8 +1383,8 @@ def review_report(report_id):
|
||||
@bp.route('/admin/forum/topic/<int:topic_id>/history')
|
||||
@login_required
|
||||
def topic_edit_history(topic_id):
|
||||
"""Admin: View topic edit history"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: View topic edit history"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -1412,8 +1412,8 @@ def topic_edit_history(topic_id):
|
||||
@bp.route('/admin/forum/reply/<int:reply_id>/history')
|
||||
@login_required
|
||||
def reply_edit_history(reply_id):
|
||||
"""Admin: View reply edit history"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: View reply edit history"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
@ -1441,8 +1441,8 @@ def reply_edit_history(reply_id):
|
||||
@bp.route('/admin/forum/deleted')
|
||||
@login_required
|
||||
def admin_deleted_content():
|
||||
"""Admin: View soft-deleted topics and replies"""
|
||||
if not current_user.is_admin:
|
||||
"""Moderator: View soft-deleted topics and replies"""
|
||||
if not current_user.can_moderate_forum():
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
@ -1553,7 +1553,7 @@ def user_forum_stats(user_id):
|
||||
@login_required
|
||||
def admin_forum_analytics():
|
||||
"""Forum analytics dashboard with stats, charts, and rankings"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
flash('Brak uprawnien do tej strony.', 'error')
|
||||
return redirect(url_for('.forum_index'))
|
||||
|
||||
@ -1787,7 +1787,7 @@ def admin_forum_analytics():
|
||||
@login_required
|
||||
def admin_forum_export_activity():
|
||||
"""Export forum activity to CSV"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
from flask import Response
|
||||
@ -1880,7 +1880,7 @@ def admin_forum_export_activity():
|
||||
@login_required
|
||||
def admin_move_topic(topic_id):
|
||||
"""Move topic to different category"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -1914,7 +1914,7 @@ def admin_move_topic(topic_id):
|
||||
@login_required
|
||||
def admin_merge_topics():
|
||||
"""Merge multiple topics into one"""
|
||||
if not current_user.is_admin:
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
@ -1986,8 +1986,8 @@ def admin_merge_topics():
|
||||
@bp.route('/admin/forum/search')
|
||||
@login_required
|
||||
def admin_forum_search():
|
||||
"""Search all forum content (including deleted) - admin only"""
|
||||
if not current_user.is_admin:
|
||||
"""Search all forum content (including deleted) - moderators only"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
query = request.args.get('q', '').strip()
|
||||
@ -2076,8 +2076,8 @@ def admin_forum_search():
|
||||
@bp.route('/admin/forum/user/<int:user_id>/activity')
|
||||
@login_required
|
||||
def admin_user_forum_activity(user_id):
|
||||
"""Get detailed forum activity for a specific user - admin only"""
|
||||
if not current_user.is_admin:
|
||||
"""Get detailed forum activity for a specific user - moderators only"""
|
||||
if not current_user.can_moderate_forum():
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
@ -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.'
|
||||
|
||||
@ -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.'
|
||||
|
||||
@ -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,
|
||||
|
||||
32
database/migrations/20260201_sync_user_roles.sql
Normal file
32
database/migrations/20260201_sync_user_roles.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- Migration: Sync user roles with is_admin flag
|
||||
-- Date: 2026-02-01
|
||||
-- Description: Ensures all users have proper role field based on is_admin and company membership
|
||||
-- Part of: Role-based access control migration from is_admin to SystemRole
|
||||
|
||||
-- 1. Set ADMIN role for users with is_admin=true
|
||||
UPDATE users
|
||||
SET role = 'ADMIN'
|
||||
WHERE is_admin = true AND (role IS NULL OR role != 'ADMIN');
|
||||
|
||||
-- 2. Set MEMBER role for non-admin users who have is_norda_member=true but no company
|
||||
UPDATE users
|
||||
SET role = 'MEMBER'
|
||||
WHERE is_admin = false
|
||||
AND is_norda_member = true
|
||||
AND company_id IS NULL
|
||||
AND (role IS NULL OR role = 'UNAFFILIATED');
|
||||
|
||||
-- 3. Set EMPLOYEE role for non-admin users who have a company assigned
|
||||
UPDATE users
|
||||
SET role = 'EMPLOYEE'
|
||||
WHERE is_admin = false
|
||||
AND company_id IS NOT NULL
|
||||
AND (role IS NULL OR role = 'UNAFFILIATED');
|
||||
|
||||
-- 4. Set UNAFFILIATED for remaining users without role
|
||||
UPDATE users
|
||||
SET role = 'UNAFFILIATED'
|
||||
WHERE role IS NULL;
|
||||
|
||||
-- 5. Verify: Show role distribution after migration
|
||||
-- SELECT role, COUNT(*) as count FROM users GROUP BY role ORDER BY role;
|
||||
@ -1339,7 +1339,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="btn-icon edit" title="{{ 'Edytuj audyt' if has_audit else 'Utwórz audyt' }}">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{% if has_audit %}
|
||||
|
||||
@ -558,7 +558,7 @@
|
||||
</svg>
|
||||
API
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<button class="btn btn-primary btn-sm" onclick="runBatchAudit()" id="batchAuditBtn">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
@ -758,7 +758,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<button class="btn-icon audit" onclick="runSingleAudit('{{ company.slug }}')" title="Uruchom audyt SEO">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
|
||||
@ -1215,7 +1215,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
||||
<!-- Admin Bar -->
|
||||
<div class="admin-bar">
|
||||
<div class="admin-bar-inner">
|
||||
|
||||
@ -375,7 +375,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
/* Reset dla pełnoekranowego chatu jak ChatGPT/Claude */
|
||||
:root {
|
||||
/* Wysokość nagłówka: 73px navbar + 36px admin bar (jeśli admin) */
|
||||
--header-height: {% if current_user.is_authenticated and current_user.is_admin %}109px{% else %}73px{% endif %};
|
||||
--header-height: {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}109px{% else %}73px{% endif %};
|
||||
}
|
||||
|
||||
html, body {
|
||||
|
||||
@ -628,7 +628,7 @@
|
||||
{% if classified.author_id == current_user.id %}
|
||||
<button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
||||
<div class="admin-actions">
|
||||
<button type="button" class="admin-btn admin-btn-toggle {% if not classified.is_active %}inactive{% endif %}" onclick="toggleActive()" title="{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
|
||||
@ -740,7 +740,7 @@
|
||||
|
||||
{# GBP Audit link - visible to admins (all profiles) or regular users (own company only) #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %}
|
||||
{% if current_user.can_edit_company(company.id) %}
|
||||
<a href="{{ url_for('gbp_audit_dashboard', slug=company.slug) }}" class="contact-bar-item gbp-audit" title="Audyt Google Business Profile">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
@ -752,7 +752,7 @@
|
||||
|
||||
{# SEO Audit link - visible to admins (all profiles) or regular users (own company only) #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %}
|
||||
{% if current_user.can_edit_company(company.id) %}
|
||||
<a href="{{ url_for('seo_audit_dashboard', slug=company.slug) }}" class="contact-bar-item seo-audit" title="Audyt SEO strony WWW">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
@ -764,7 +764,7 @@
|
||||
|
||||
{# Social Media Audit link - visible to admins (all profiles) or regular users (own company only) #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %}
|
||||
{% if current_user.can_edit_company(company.id) %}
|
||||
<a href="{{ url_for('social_audit_dashboard', slug=company.slug) }}" class="contact-bar-item social-audit" title="Audyt Social Media">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
|
||||
@ -776,7 +776,7 @@
|
||||
|
||||
{# IT Infrastructure Audit link - visible to admins (all profiles) or regular users (own company only) #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin or (current_user.company_id and current_user.company_id == company.id) %}
|
||||
{% if current_user.can_edit_company(company.id) %}
|
||||
<a href="{{ url_for('it_audit_form', company_id=company.id) }}" class="contact-bar-item it-audit" title="Audyt Infrastruktury IT">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
|
||||
|
||||
@ -388,7 +388,7 @@
|
||||
</h2>
|
||||
<p style="color: var(--text-secondary); margin: 8px 0 0 0;">
|
||||
Witaj, <strong>{{ current_user.name }}</strong>!
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<span class="badge-admin">Administrator</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
@ -508,7 +508,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<!-- Admin Section -->
|
||||
<div class="admin-section-highlight">
|
||||
<h4 style="margin: 0 0 var(--spacing-lg, 20px) 0; display: flex; align-items: center; gap: 8px;">
|
||||
@ -660,7 +660,7 @@
|
||||
<p style="margin: 0 0 4px 0;"><strong>Email:</strong> {{ current_user.email }}</p>
|
||||
<p style="margin: 0;">
|
||||
<strong>Rola:</strong>
|
||||
{% if current_user.is_admin %}
|
||||
{% if current_user.can_access_admin_panel() %}
|
||||
<span class="badge-admin">Administrator</span>
|
||||
{% else %}
|
||||
<span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem;">Użytkownik</span>
|
||||
|
||||
@ -986,7 +986,7 @@
|
||||
</span>
|
||||
{{ topic.title }}
|
||||
</h1>
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_moderate_forum() %}
|
||||
<div class="admin-actions">
|
||||
<button type="button" class="admin-btn admin-btn-pin" onclick="togglePin({{ topic.id }})" title="{% if topic.is_pinned %}Odepnij{% else %}Przypnij{% endif %}">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
@ -1090,7 +1090,7 @@
|
||||
<!-- User actions for topic -->
|
||||
{% if not topic.is_locked %}
|
||||
<div class="user-actions">
|
||||
{% if topic.author_id == current_user.id or current_user.is_admin %}
|
||||
{% if topic.author_id == current_user.id or current_user.can_moderate_forum() %}
|
||||
<button type="button" class="action-btn" onclick="openEditModal('topic', {{ topic.id }}, document.getElementById('topicContent').innerText)">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
Edytuj
|
||||
@ -1151,7 +1151,7 @@
|
||||
<span class="edited-badge">(edytowano)</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_moderate_forum() %}
|
||||
<div class="reply-admin-actions">
|
||||
<button type="button" class="admin-btn admin-btn-sm" onclick="toggleSolution({{ reply.id }})" title="{% if reply.is_solution %}Usuń oznaczenie{% else %}Oznacz jako rozwiązanie{% endif %}">
|
||||
✓
|
||||
|
||||
@ -263,7 +263,7 @@
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div class="release-date">{{ release.date }}</div>
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
||||
<button class="notify-btn" onclick="notifyRelease('{{ release.version }}', this)" title="Wyślij powiadomienia o tej wersji">
|
||||
🔔 Powiadom
|
||||
</button>
|
||||
@ -472,7 +472,7 @@ document.getElementById('confirmModal').addEventListener('click', function(e) {
|
||||
}
|
||||
});
|
||||
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
||||
function notifyRelease(version, btn) {
|
||||
showConfirmModal(
|
||||
'Wyślij powiadomienia',
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user