security(permissions): restrict guest access to members-only areas
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Forum: add @forum_access_required to ALL public routes (read+write) - Reports: add @member_required to all report routes - Announcements: add @member_required to list and detail - Education: add @member_required to all routes - Calendar: guests can VIEW all events but cannot RSVP (public+members_only) - PEJ and ZOPK remain accessible (as intended for outreach) UNAFFILIATED users (registered but not Izba members) are now properly restricted from internal community features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8f18c94e5
commit
7b31e6ba44
@ -7,12 +7,14 @@ Platforma Edukacyjna - materiały szkoleniowe dla członków Norda Biznes.
|
||||
|
||||
from flask import render_template, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from utils.decorators import member_required
|
||||
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/', endpoint='education_index')
|
||||
@login_required
|
||||
@member_required
|
||||
def index():
|
||||
"""Strona główna Platformy Edukacyjnej."""
|
||||
materials = [
|
||||
@ -57,6 +59,7 @@ def index():
|
||||
|
||||
@bp.route('/opinie-google', endpoint='google_reviews_guide')
|
||||
@login_required
|
||||
@member_required
|
||||
def google_reviews_guide():
|
||||
"""Poradnik o opiniach Google dla członków Izby."""
|
||||
from database import SessionLocal, CompanyWebsiteAnalysis, Company
|
||||
|
||||
@ -18,6 +18,7 @@ from database import (
|
||||
ForumTopicRead, ForumReplyRead
|
||||
)
|
||||
from utils.helpers import sanitize_input
|
||||
from utils.decorators import forum_access_required
|
||||
from utils.notifications import (
|
||||
create_forum_reply_notification,
|
||||
create_forum_reaction_notification,
|
||||
@ -49,6 +50,7 @@ except ImportError:
|
||||
|
||||
@bp.route('/forum')
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def forum_index():
|
||||
"""Forum - list of topics with category/status/solution filters and search"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
@ -121,6 +123,7 @@ def forum_index():
|
||||
|
||||
@bp.route('/forum/nowy', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def forum_new_topic():
|
||||
"""Create new forum topic with category and attachments"""
|
||||
if request.method == 'POST':
|
||||
@ -212,6 +215,7 @@ def forum_new_topic():
|
||||
|
||||
@bp.route('/forum/<int:topic_id>')
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def forum_topic(topic_id):
|
||||
"""View forum topic with replies"""
|
||||
db = SessionLocal()
|
||||
@ -296,6 +300,7 @@ def forum_topic(topic_id):
|
||||
|
||||
@bp.route('/forum/<int:topic_id>/odpowiedz', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def forum_reply(topic_id):
|
||||
"""Add reply to forum topic with optional attachment"""
|
||||
content = request.form.get('content', '').strip()
|
||||
@ -765,6 +770,7 @@ def _can_edit_content(content_created_at):
|
||||
|
||||
@bp.route('/forum/topic/<int:topic_id>/edit', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def edit_topic(topic_id):
|
||||
"""Edit own topic (within 24h)"""
|
||||
data = request.get_json() or {}
|
||||
@ -821,6 +827,7 @@ def edit_topic(topic_id):
|
||||
|
||||
@bp.route('/forum/reply/<int:reply_id>/edit', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def edit_reply(reply_id):
|
||||
"""Edit own reply (within 24h)"""
|
||||
data = request.get_json() or {}
|
||||
@ -879,6 +886,7 @@ def edit_reply(reply_id):
|
||||
|
||||
@bp.route('/forum/reply/<int:reply_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def delete_own_reply(reply_id):
|
||||
"""Soft delete own reply (if no child responses exist)"""
|
||||
db = SessionLocal()
|
||||
@ -913,6 +921,7 @@ def delete_own_reply(reply_id):
|
||||
|
||||
@bp.route('/forum/topic/<int:topic_id>/react', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def react_to_topic(topic_id):
|
||||
"""Add or remove reaction from topic"""
|
||||
data = request.get_json() or {}
|
||||
@ -977,6 +986,7 @@ def react_to_topic(topic_id):
|
||||
|
||||
@bp.route('/forum/reply/<int:reply_id>/react', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def react_to_reply(reply_id):
|
||||
"""Add or remove reaction from reply"""
|
||||
data = request.get_json() or {}
|
||||
@ -1056,6 +1066,7 @@ def react_to_reply(reply_id):
|
||||
|
||||
@bp.route('/forum/topic/<int:topic_id>/subscribe', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def subscribe_to_topic(topic_id):
|
||||
"""Subscribe to topic notifications"""
|
||||
db = SessionLocal()
|
||||
@ -1091,6 +1102,7 @@ def subscribe_to_topic(topic_id):
|
||||
|
||||
@bp.route('/forum/topic/<int:topic_id>/unsubscribe', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def unsubscribe_from_topic(topic_id):
|
||||
"""Unsubscribe from topic notifications"""
|
||||
db = SessionLocal()
|
||||
@ -1117,6 +1129,7 @@ def unsubscribe_from_topic(topic_id):
|
||||
|
||||
@bp.route('/forum/<int:topic_id>/unsubscribe', methods=['GET'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def unsubscribe_from_email(topic_id):
|
||||
"""Unsubscribe from topic via email link (GET, requires login)"""
|
||||
db = SessionLocal()
|
||||
@ -1143,6 +1156,7 @@ def unsubscribe_from_email(topic_id):
|
||||
|
||||
@bp.route('/forum/report', methods=['POST'])
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def report_content():
|
||||
"""Report topic or reply for moderation"""
|
||||
data = request.get_json() or {}
|
||||
@ -1598,6 +1612,7 @@ def admin_deleted_content():
|
||||
|
||||
@bp.route('/forum/user/<int:user_id>/stats')
|
||||
@login_required
|
||||
@forum_access_required
|
||||
def user_forum_stats(user_id):
|
||||
"""Get forum statistics for a user (for tooltip display)"""
|
||||
from sqlalchemy import func
|
||||
|
||||
@ -10,6 +10,7 @@ from datetime import datetime
|
||||
|
||||
from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user, login_required
|
||||
from utils.decorators import member_required
|
||||
from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import array as pg_array
|
||||
|
||||
@ -25,6 +26,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/ogloszenia')
|
||||
@login_required
|
||||
@member_required
|
||||
def announcements_list():
|
||||
"""Strona z listą ogłoszeń dla zalogowanych członków"""
|
||||
db = SessionLocal()
|
||||
@ -73,6 +75,7 @@ def announcements_list():
|
||||
|
||||
@bp.route('/ogloszenia/<slug>')
|
||||
@login_required
|
||||
@member_required
|
||||
def announcement_detail(slug):
|
||||
"""Szczegóły ogłoszenia dla zalogowanych członków"""
|
||||
db = SessionLocal()
|
||||
|
||||
@ -8,6 +8,7 @@ Business analytics and reporting endpoints.
|
||||
from datetime import datetime, date
|
||||
from flask import render_template, url_for
|
||||
from flask_login import login_required
|
||||
from utils.decorators import member_required
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
@ -17,6 +18,7 @@ from database import SessionLocal, Company, Category, CompanySocialMedia
|
||||
|
||||
@bp.route('/', endpoint='reports_index')
|
||||
@login_required
|
||||
@member_required
|
||||
def index():
|
||||
"""Lista dostępnych raportów."""
|
||||
reports = [
|
||||
@ -47,6 +49,7 @@ def index():
|
||||
|
||||
@bp.route('/staz-czlonkostwa', endpoint='report_membership')
|
||||
@login_required
|
||||
@member_required
|
||||
def membership():
|
||||
"""Raport: Staż członkostwa w Izbie NORDA."""
|
||||
db = SessionLocal()
|
||||
@ -91,6 +94,7 @@ def membership():
|
||||
|
||||
@bp.route('/social-media', endpoint='report_social_media')
|
||||
@login_required
|
||||
@member_required
|
||||
def social_media():
|
||||
"""Raport: Pokrycie Social Media."""
|
||||
db = SessionLocal()
|
||||
@ -141,6 +145,7 @@ def social_media():
|
||||
|
||||
@bp.route('/struktura-branzowa', endpoint='report_categories')
|
||||
@login_required
|
||||
@member_required
|
||||
def categories():
|
||||
"""Raport: Struktura branżowa."""
|
||||
db = SessionLocal()
|
||||
|
||||
@ -2215,10 +2215,11 @@ class NordaEvent(Base):
|
||||
if access == 'public':
|
||||
return True
|
||||
elif access == 'members_only':
|
||||
return user.is_norda_member or user.has_role(SystemRole.MEMBER)
|
||||
# Guests (UNAFFILIATED) can also VIEW events — but cannot register (see can_user_attend)
|
||||
return True
|
||||
elif access == 'rada_only':
|
||||
# Wszyscy członkowie WIDZĄ wydarzenia Rady Izby (tytuł, data, miejsce)
|
||||
# ale nie mogą dołączyć ani zobaczyć uczestników
|
||||
# ale nie mogą dołączyć ani zobaczyć uczestników. Goście nie widzą.
|
||||
return user.is_norda_member or user.has_role(SystemRole.MEMBER) or user.is_rada_member
|
||||
else:
|
||||
return False
|
||||
@ -2227,6 +2228,7 @@ class NordaEvent(Base):
|
||||
"""Check if a user can register for this event.
|
||||
|
||||
For Rada Izby events, only designated board members can register.
|
||||
UNAFFILIATED guests can view events but cannot register for any.
|
||||
"""
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
@ -2238,7 +2240,8 @@ class NordaEvent(Base):
|
||||
access = self.access_level or 'members_only'
|
||||
|
||||
if access == 'public':
|
||||
return True
|
||||
# Guests can view but not register — only members can RSVP
|
||||
return user.is_norda_member or user.has_role(SystemRole.MEMBER)
|
||||
elif access == 'members_only':
|
||||
return user.is_norda_member or user.has_role(SystemRole.MEMBER)
|
||||
elif access == 'rada_only':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user