From 6bf243d1cbb645fe026e5ab86fd5e7f06d41f8c6 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Sun, 1 Feb 2026 21:33:27 +0100 Subject: [PATCH] security: Restrict member-only features to MEMBER role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modules now requiring MEMBER role or higher: - NordaGPT (/chat) - with dedicated landing page for non-members - Wiadomości (/wiadomosci) - private messaging - Tablica B2B (/tablica) - business classifieds - Kontakty (/kontakty) - member contact information Non-members see a promotional page explaining the benefits of NordaGPT membership instead of being simply redirected. This provides clear value proposition for NORDA membership while protecting member-exclusive features. Co-Authored-By: Claude Opus 4.5 --- blueprints/chat/routes.py | 16 +- blueprints/community/classifieds/routes.py | 13 ++ blueprints/community/contacts/routes.py | 7 + blueprints/messages/routes.py | 12 ++ templates/chat_members_only.html | 169 +++++++++++++++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 templates/chat_members_only.html diff --git a/blueprints/chat/routes.py b/blueprints/chat/routes.py index 63d0c10..34dcf25 100644 --- a/blueprints/chat/routes.py +++ b/blueprints/chat/routes.py @@ -14,10 +14,12 @@ from sqlalchemy import func, desc from . import bp from database import ( - SessionLocal, AIChatConversation, AIChatMessage, AIChatFeedback, AIAPICostLog + SessionLocal, AIChatConversation, AIChatMessage, AIChatFeedback, AIAPICostLog, + SystemRole ) from nordabiz_chat import NordaBizChatEngine from extensions import csrf +from utils.decorators import member_required # Logger logger = logging.getLogger(__name__) @@ -30,13 +32,17 @@ logger = logging.getLogger(__name__) @bp.route('/chat') @login_required def chat(): - """AI Chat interface""" + """AI Chat interface - requires MEMBER role""" + # SECURITY: NordaGPT is only for members (MEMBER role or higher) + if not current_user.has_role(SystemRole.MEMBER): + return render_template('chat_members_only.html'), 403 return render_template('chat.html') @bp.route('/api/chat/settings', methods=['GET', 'POST']) @csrf.exempt @login_required +@member_required def chat_settings(): """Get or update chat settings (model selection, monthly cost)""" if request.method == 'GET': @@ -92,6 +98,7 @@ def chat_settings(): @bp.route('/api/chat/start', methods=['POST']) @csrf.exempt @login_required +@member_required def chat_start(): """Start new chat conversation""" try: @@ -118,6 +125,7 @@ def chat_start(): @bp.route('/api/chat//message', methods=['POST']) @csrf.exempt @login_required +@member_required def chat_send_message(conversation_id): """Send message to AI chat""" try: @@ -231,6 +239,7 @@ def chat_send_message(conversation_id): @bp.route('/api/chat//history', methods=['GET']) @login_required +@member_required def chat_get_history(conversation_id): """Get conversation history""" try: @@ -263,6 +272,7 @@ def chat_get_history(conversation_id): @bp.route('/api/chat/conversations', methods=['GET']) @login_required +@member_required def chat_list_conversations(): """Get list of user's conversations for sidebar""" db = SessionLocal() @@ -293,6 +303,7 @@ def chat_list_conversations(): @bp.route('/api/chat//delete', methods=['DELETE']) @login_required +@member_required def chat_delete_conversation(conversation_id): """Delete a conversation""" db = SessionLocal() @@ -325,6 +336,7 @@ def chat_delete_conversation(conversation_id): @bp.route('/api/chat/feedback', methods=['POST']) @login_required +@member_required def chat_feedback(): """API: Submit feedback for AI response""" try: diff --git a/blueprints/community/classifieds/routes.py b/blueprints/community/classifieds/routes.py index 88147e6..79ff23c 100644 --- a/blueprints/community/classifieds/routes.py +++ b/blueprints/community/classifieds/routes.py @@ -13,10 +13,12 @@ from . import bp from database import SessionLocal, Classified, ClassifiedRead, ClassifiedInterest, ClassifiedQuestion, User from sqlalchemy import desc from utils.helpers import sanitize_input +from utils.decorators import member_required @bp.route('/', endpoint='classifieds_index') @login_required +@member_required def index(): """Tablica ogłoszeń B2B""" listing_type = request.args.get('type', '') @@ -65,6 +67,7 @@ def index(): @bp.route('/nowe', methods=['GET', 'POST'], endpoint='classifieds_new') @login_required +@member_required def new(): """Dodaj nowe ogłoszenie""" if request.method == 'POST': @@ -108,6 +111,7 @@ def new(): @bp.route('/', endpoint='classifieds_view') @login_required +@member_required def view(classified_id): """Szczegóły ogłoszenia""" db = SessionLocal() @@ -185,6 +189,7 @@ def view(classified_id): @bp.route('//zakoncz', methods=['POST'], endpoint='classifieds_close') @login_required +@member_required def close(classified_id): """Zamknij ogłoszenie""" db = SessionLocal() @@ -207,6 +212,7 @@ def close(classified_id): @bp.route('//delete', methods=['POST'], endpoint='classifieds_delete') @login_required +@member_required def delete(classified_id): """Usuń ogłoszenie (admin only)""" if not current_user.can_access_admin_panel(): @@ -231,6 +237,7 @@ def delete(classified_id): @bp.route('//toggle-active', methods=['POST'], endpoint='classifieds_toggle_active') @login_required +@member_required def toggle_active(classified_id): """Aktywuj/dezaktywuj ogłoszenie (admin only)""" if not current_user.can_access_admin_panel(): @@ -260,6 +267,7 @@ def toggle_active(classified_id): @bp.route('//interest', methods=['POST'], endpoint='classifieds_interest') @login_required +@member_required def toggle_interest(classified_id): """Toggle zainteresowania ogłoszeniem""" db = SessionLocal() @@ -312,6 +320,7 @@ def toggle_interest(classified_id): @bp.route('//interests', endpoint='classifieds_interests') @login_required +@member_required def list_interests(classified_id): """Lista zainteresowanych (tylko dla autora ogłoszenia)""" db = SessionLocal() @@ -357,6 +366,7 @@ def list_interests(classified_id): @bp.route('//ask', methods=['POST'], endpoint='classifieds_ask') @login_required +@member_required def ask_question(classified_id): """Zadaj pytanie do ogłoszenia""" db = SessionLocal() @@ -405,6 +415,7 @@ def ask_question(classified_id): @bp.route('//question//answer', methods=['POST'], endpoint='classifieds_answer') @login_required +@member_required def answer_question(classified_id, question_id): """Odpowiedz na pytanie (tylko autor ogłoszenia)""" db = SessionLocal() @@ -457,6 +468,7 @@ def answer_question(classified_id, question_id): @bp.route('//question//hide', methods=['POST'], endpoint='classifieds_hide_question') @login_required +@member_required def hide_question(classified_id, question_id): """Ukryj/pokaż pytanie (tylko autor ogłoszenia)""" db = SessionLocal() @@ -495,6 +507,7 @@ def hide_question(classified_id, question_id): @bp.route('//questions', endpoint='classifieds_questions') @login_required +@member_required def list_questions(classified_id): """Lista pytań i odpowiedzi do ogłoszenia""" db = SessionLocal() diff --git a/blueprints/community/contacts/routes.py b/blueprints/community/contacts/routes.py index 77c615a..f1f6b80 100644 --- a/blueprints/community/contacts/routes.py +++ b/blueprints/community/contacts/routes.py @@ -13,6 +13,8 @@ from flask import render_template, request, redirect, url_for, flash, current_ap from flask_login import login_required, current_user from sqlalchemy import or_ +from utils.decorators import member_required + from . import bp from database import SessionLocal, ExternalContact @@ -21,6 +23,7 @@ logger = logging.getLogger(__name__) @bp.route('/', endpoint='contacts_list') @login_required +@member_required def list(): """ Lista kontaktów zewnętrznych - urzędy, instytucje, partnerzy. @@ -95,6 +98,7 @@ def list(): @bp.route('/', endpoint='contact_detail') @login_required +@member_required def detail(contact_id): """ Szczegóły kontaktu zewnętrznego - pełna karta osoby. @@ -133,6 +137,7 @@ def detail(contact_id): @bp.route('/dodaj', methods=['GET', 'POST'], endpoint='contact_add') @login_required +@member_required def add(): """ Dodawanie nowego kontaktu zewnętrznego. @@ -198,6 +203,7 @@ def add(): @bp.route('//edytuj', methods=['GET', 'POST'], endpoint='contact_edit') @login_required +@member_required def edit(contact_id): """ Edycja kontaktu zewnętrznego. @@ -267,6 +273,7 @@ def edit(contact_id): @bp.route('//usun', methods=['POST'], endpoint='contact_delete') @login_required +@member_required def delete(contact_id): """ Usuwanie kontaktu zewnętrznego (soft delete). diff --git a/blueprints/messages/routes.py b/blueprints/messages/routes.py index 277882f..290b9b7 100644 --- a/blueprints/messages/routes.py +++ b/blueprints/messages/routes.py @@ -13,6 +13,7 @@ from flask_login import login_required, current_user from . import bp from database import SessionLocal, User, PrivateMessage, UserNotification, UserBlock, Classified from utils.helpers import sanitize_input +from utils.decorators import member_required # ============================================================ @@ -21,6 +22,7 @@ from utils.helpers import sanitize_input @bp.route('/wiadomosci') @login_required +@member_required def messages_inbox(): """Skrzynka odbiorcza""" page = request.args.get('page', 1, type=int) @@ -52,6 +54,7 @@ def messages_inbox(): @bp.route('/wiadomosci/wyslane') @login_required +@member_required def messages_sent(): """Wysłane wiadomości""" page = request.args.get('page', 1, type=int) @@ -77,6 +80,7 @@ def messages_sent(): @bp.route('/wiadomosci/nowa') @login_required +@member_required def messages_new(): """Formularz nowej wiadomości""" recipient_id = request.args.get('to', type=int) @@ -124,6 +128,7 @@ def messages_new(): @bp.route('/wiadomosci/wyslij', methods=['POST']) @login_required +@member_required def messages_send(): """Wyślij wiadomość""" recipient_id = request.form.get('recipient_id', type=int) @@ -171,6 +176,7 @@ def messages_send(): @bp.route('/wiadomosci/') @login_required +@member_required def messages_view(message_id): """Czytaj wiadomość""" db = SessionLocal() @@ -214,6 +220,7 @@ def messages_view(message_id): @bp.route('/wiadomosci//odpowiedz', methods=['POST']) @login_required +@member_required def messages_reply(message_id): """Odpowiedz na wiadomość""" content = request.form.get('content', '').strip() @@ -262,6 +269,7 @@ def messages_reply(message_id): @bp.route('/api/messages/unread-count') @login_required +@member_required def api_unread_count(): """API: Liczba nieprzeczytanych wiadomości""" db = SessionLocal() @@ -281,6 +289,7 @@ def api_unread_count(): @bp.route('/api/notifications') @login_required +@member_required def api_notifications(): """API: Get user notifications""" limit = request.args.get('limit', 20, type=int) @@ -330,6 +339,7 @@ def api_notifications(): @bp.route('/api/notifications//read', methods=['POST']) @login_required +@member_required def api_notification_mark_read(notification_id): """API: Mark notification as read""" db = SessionLocal() @@ -355,6 +365,7 @@ def api_notification_mark_read(notification_id): @bp.route('/api/notifications/read-all', methods=['POST']) @login_required +@member_required def api_notifications_mark_all_read(): """API: Mark all notifications as read""" db = SessionLocal() @@ -379,6 +390,7 @@ def api_notifications_mark_all_read(): @bp.route('/api/notifications/unread-count') @login_required +@member_required def api_notifications_unread_count(): """API: Get unread notifications count""" db = SessionLocal() diff --git a/templates/chat_members_only.html b/templates/chat_members_only.html new file mode 100644 index 0000000..aa80b26 --- /dev/null +++ b/templates/chat_members_only.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block title %}NordaGPT - Dla Członków Izby{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ 🤖 + +

NordaGPT - Asystent AI dla Członków

+ +

+ NordaGPT to inteligentny asystent, który zna wszystkie firmy członkowskie Izby NORDA.
+ Ta funkcja jest dostępna wyłącznie dla członków stowarzyszenia. +

+ +
+
+
🔍
+
Wyszukiwanie firm
+
+ Znajdź partnerów biznesowych po usługach, kompetencjach lub lokalizacji +
+
+ +
+
💬
+
Naturalna rozmowa
+
+ Zadawaj pytania w języku naturalnym - AI zrozumie Twoje potrzeby +
+
+ +
+
📊
+
Pełna baza wiedzy
+
+ Dostęp do aktualności, wydarzeń, ogłoszeń B2B i dyskusji na forum +
+
+ +
+
🤝
+
Rekomendacje
+
+ Poznaj opinie innych członków o firmach i ich usługach +
+
+
+ +
+
Chcesz korzystać z NordaGPT?
+

+ Dołącz do Izby Przedsiębiorców NORDA i zyskaj dostęp do asystenta AI oraz wszystkich korzyści członkostwa. +

+ + Dowiedz się więcej o członkostwie + +
+ + + ← Wróć do katalogu firm + +
+{% endblock %}