security: Restrict member-only features to MEMBER role
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 <noreply@anthropic.com>
This commit is contained in:
parent
579b4636bc
commit
6bf243d1cb
@ -14,10 +14,12 @@ from sqlalchemy import func, desc
|
|||||||
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from database import (
|
from database import (
|
||||||
SessionLocal, AIChatConversation, AIChatMessage, AIChatFeedback, AIAPICostLog
|
SessionLocal, AIChatConversation, AIChatMessage, AIChatFeedback, AIAPICostLog,
|
||||||
|
SystemRole
|
||||||
)
|
)
|
||||||
from nordabiz_chat import NordaBizChatEngine
|
from nordabiz_chat import NordaBizChatEngine
|
||||||
from extensions import csrf
|
from extensions import csrf
|
||||||
|
from utils.decorators import member_required
|
||||||
|
|
||||||
# Logger
|
# Logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -30,13 +32,17 @@ logger = logging.getLogger(__name__)
|
|||||||
@bp.route('/chat')
|
@bp.route('/chat')
|
||||||
@login_required
|
@login_required
|
||||||
def chat():
|
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')
|
return render_template('chat.html')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/chat/settings', methods=['GET', 'POST'])
|
@bp.route('/api/chat/settings', methods=['GET', 'POST'])
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_settings():
|
def chat_settings():
|
||||||
"""Get or update chat settings (model selection, monthly cost)"""
|
"""Get or update chat settings (model selection, monthly cost)"""
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@ -92,6 +98,7 @@ def chat_settings():
|
|||||||
@bp.route('/api/chat/start', methods=['POST'])
|
@bp.route('/api/chat/start', methods=['POST'])
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_start():
|
def chat_start():
|
||||||
"""Start new chat conversation"""
|
"""Start new chat conversation"""
|
||||||
try:
|
try:
|
||||||
@ -118,6 +125,7 @@ def chat_start():
|
|||||||
@bp.route('/api/chat/<int:conversation_id>/message', methods=['POST'])
|
@bp.route('/api/chat/<int:conversation_id>/message', methods=['POST'])
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_send_message(conversation_id):
|
def chat_send_message(conversation_id):
|
||||||
"""Send message to AI chat"""
|
"""Send message to AI chat"""
|
||||||
try:
|
try:
|
||||||
@ -231,6 +239,7 @@ def chat_send_message(conversation_id):
|
|||||||
|
|
||||||
@bp.route('/api/chat/<int:conversation_id>/history', methods=['GET'])
|
@bp.route('/api/chat/<int:conversation_id>/history', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_get_history(conversation_id):
|
def chat_get_history(conversation_id):
|
||||||
"""Get conversation history"""
|
"""Get conversation history"""
|
||||||
try:
|
try:
|
||||||
@ -263,6 +272,7 @@ def chat_get_history(conversation_id):
|
|||||||
|
|
||||||
@bp.route('/api/chat/conversations', methods=['GET'])
|
@bp.route('/api/chat/conversations', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_list_conversations():
|
def chat_list_conversations():
|
||||||
"""Get list of user's conversations for sidebar"""
|
"""Get list of user's conversations for sidebar"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -293,6 +303,7 @@ def chat_list_conversations():
|
|||||||
|
|
||||||
@bp.route('/api/chat/<int:conversation_id>/delete', methods=['DELETE'])
|
@bp.route('/api/chat/<int:conversation_id>/delete', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_delete_conversation(conversation_id):
|
def chat_delete_conversation(conversation_id):
|
||||||
"""Delete a conversation"""
|
"""Delete a conversation"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -325,6 +336,7 @@ def chat_delete_conversation(conversation_id):
|
|||||||
|
|
||||||
@bp.route('/api/chat/feedback', methods=['POST'])
|
@bp.route('/api/chat/feedback', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def chat_feedback():
|
def chat_feedback():
|
||||||
"""API: Submit feedback for AI response"""
|
"""API: Submit feedback for AI response"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -13,10 +13,12 @@ from . import bp
|
|||||||
from database import SessionLocal, Classified, ClassifiedRead, ClassifiedInterest, ClassifiedQuestion, User
|
from database import SessionLocal, Classified, ClassifiedRead, ClassifiedInterest, ClassifiedQuestion, User
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
from utils.helpers import sanitize_input
|
from utils.helpers import sanitize_input
|
||||||
|
from utils.decorators import member_required
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', endpoint='classifieds_index')
|
@bp.route('/', endpoint='classifieds_index')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def index():
|
def index():
|
||||||
"""Tablica ogłoszeń B2B"""
|
"""Tablica ogłoszeń B2B"""
|
||||||
listing_type = request.args.get('type', '')
|
listing_type = request.args.get('type', '')
|
||||||
@ -65,6 +67,7 @@ def index():
|
|||||||
|
|
||||||
@bp.route('/nowe', methods=['GET', 'POST'], endpoint='classifieds_new')
|
@bp.route('/nowe', methods=['GET', 'POST'], endpoint='classifieds_new')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def new():
|
def new():
|
||||||
"""Dodaj nowe ogłoszenie"""
|
"""Dodaj nowe ogłoszenie"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -108,6 +111,7 @@ def new():
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>', endpoint='classifieds_view')
|
@bp.route('/<int:classified_id>', endpoint='classifieds_view')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def view(classified_id):
|
def view(classified_id):
|
||||||
"""Szczegóły ogłoszenia"""
|
"""Szczegóły ogłoszenia"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -185,6 +189,7 @@ def view(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/zakoncz', methods=['POST'], endpoint='classifieds_close')
|
@bp.route('/<int:classified_id>/zakoncz', methods=['POST'], endpoint='classifieds_close')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def close(classified_id):
|
def close(classified_id):
|
||||||
"""Zamknij ogłoszenie"""
|
"""Zamknij ogłoszenie"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -207,6 +212,7 @@ def close(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/delete', methods=['POST'], endpoint='classifieds_delete')
|
@bp.route('/<int:classified_id>/delete', methods=['POST'], endpoint='classifieds_delete')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def delete(classified_id):
|
def delete(classified_id):
|
||||||
"""Usuń ogłoszenie (admin only)"""
|
"""Usuń ogłoszenie (admin only)"""
|
||||||
if not current_user.can_access_admin_panel():
|
if not current_user.can_access_admin_panel():
|
||||||
@ -231,6 +237,7 @@ def delete(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/toggle-active', methods=['POST'], endpoint='classifieds_toggle_active')
|
@bp.route('/<int:classified_id>/toggle-active', methods=['POST'], endpoint='classifieds_toggle_active')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def toggle_active(classified_id):
|
def toggle_active(classified_id):
|
||||||
"""Aktywuj/dezaktywuj ogłoszenie (admin only)"""
|
"""Aktywuj/dezaktywuj ogłoszenie (admin only)"""
|
||||||
if not current_user.can_access_admin_panel():
|
if not current_user.can_access_admin_panel():
|
||||||
@ -260,6 +267,7 @@ def toggle_active(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/interest', methods=['POST'], endpoint='classifieds_interest')
|
@bp.route('/<int:classified_id>/interest', methods=['POST'], endpoint='classifieds_interest')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def toggle_interest(classified_id):
|
def toggle_interest(classified_id):
|
||||||
"""Toggle zainteresowania ogłoszeniem"""
|
"""Toggle zainteresowania ogłoszeniem"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -312,6 +320,7 @@ def toggle_interest(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/interests', endpoint='classifieds_interests')
|
@bp.route('/<int:classified_id>/interests', endpoint='classifieds_interests')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def list_interests(classified_id):
|
def list_interests(classified_id):
|
||||||
"""Lista zainteresowanych (tylko dla autora ogłoszenia)"""
|
"""Lista zainteresowanych (tylko dla autora ogłoszenia)"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -357,6 +366,7 @@ def list_interests(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/ask', methods=['POST'], endpoint='classifieds_ask')
|
@bp.route('/<int:classified_id>/ask', methods=['POST'], endpoint='classifieds_ask')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def ask_question(classified_id):
|
def ask_question(classified_id):
|
||||||
"""Zadaj pytanie do ogłoszenia"""
|
"""Zadaj pytanie do ogłoszenia"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -405,6 +415,7 @@ def ask_question(classified_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/question/<int:question_id>/answer', methods=['POST'], endpoint='classifieds_answer')
|
@bp.route('/<int:classified_id>/question/<int:question_id>/answer', methods=['POST'], endpoint='classifieds_answer')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def answer_question(classified_id, question_id):
|
def answer_question(classified_id, question_id):
|
||||||
"""Odpowiedz na pytanie (tylko autor ogłoszenia)"""
|
"""Odpowiedz na pytanie (tylko autor ogłoszenia)"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -457,6 +468,7 @@ def answer_question(classified_id, question_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/question/<int:question_id>/hide', methods=['POST'], endpoint='classifieds_hide_question')
|
@bp.route('/<int:classified_id>/question/<int:question_id>/hide', methods=['POST'], endpoint='classifieds_hide_question')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def hide_question(classified_id, question_id):
|
def hide_question(classified_id, question_id):
|
||||||
"""Ukryj/pokaż pytanie (tylko autor ogłoszenia)"""
|
"""Ukryj/pokaż pytanie (tylko autor ogłoszenia)"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -495,6 +507,7 @@ def hide_question(classified_id, question_id):
|
|||||||
|
|
||||||
@bp.route('/<int:classified_id>/questions', endpoint='classifieds_questions')
|
@bp.route('/<int:classified_id>/questions', endpoint='classifieds_questions')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def list_questions(classified_id):
|
def list_questions(classified_id):
|
||||||
"""Lista pytań i odpowiedzi do ogłoszenia"""
|
"""Lista pytań i odpowiedzi do ogłoszenia"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|||||||
@ -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 flask_login import login_required, current_user
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from utils.decorators import member_required
|
||||||
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from database import SessionLocal, ExternalContact
|
from database import SessionLocal, ExternalContact
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@bp.route('/', endpoint='contacts_list')
|
@bp.route('/', endpoint='contacts_list')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def list():
|
def list():
|
||||||
"""
|
"""
|
||||||
Lista kontaktów zewnętrznych - urzędy, instytucje, partnerzy.
|
Lista kontaktów zewnętrznych - urzędy, instytucje, partnerzy.
|
||||||
@ -95,6 +98,7 @@ def list():
|
|||||||
|
|
||||||
@bp.route('/<int:contact_id>', endpoint='contact_detail')
|
@bp.route('/<int:contact_id>', endpoint='contact_detail')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def detail(contact_id):
|
def detail(contact_id):
|
||||||
"""
|
"""
|
||||||
Szczegóły kontaktu zewnętrznego - pełna karta osoby.
|
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')
|
@bp.route('/dodaj', methods=['GET', 'POST'], endpoint='contact_add')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def add():
|
def add():
|
||||||
"""
|
"""
|
||||||
Dodawanie nowego kontaktu zewnętrznego.
|
Dodawanie nowego kontaktu zewnętrznego.
|
||||||
@ -198,6 +203,7 @@ def add():
|
|||||||
|
|
||||||
@bp.route('/<int:contact_id>/edytuj', methods=['GET', 'POST'], endpoint='contact_edit')
|
@bp.route('/<int:contact_id>/edytuj', methods=['GET', 'POST'], endpoint='contact_edit')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def edit(contact_id):
|
def edit(contact_id):
|
||||||
"""
|
"""
|
||||||
Edycja kontaktu zewnętrznego.
|
Edycja kontaktu zewnętrznego.
|
||||||
@ -267,6 +273,7 @@ def edit(contact_id):
|
|||||||
|
|
||||||
@bp.route('/<int:contact_id>/usun', methods=['POST'], endpoint='contact_delete')
|
@bp.route('/<int:contact_id>/usun', methods=['POST'], endpoint='contact_delete')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def delete(contact_id):
|
def delete(contact_id):
|
||||||
"""
|
"""
|
||||||
Usuwanie kontaktu zewnętrznego (soft delete).
|
Usuwanie kontaktu zewnętrznego (soft delete).
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from flask_login import login_required, current_user
|
|||||||
from . import bp
|
from . import bp
|
||||||
from database import SessionLocal, User, PrivateMessage, UserNotification, UserBlock, Classified
|
from database import SessionLocal, User, PrivateMessage, UserNotification, UserBlock, Classified
|
||||||
from utils.helpers import sanitize_input
|
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')
|
@bp.route('/wiadomosci')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_inbox():
|
def messages_inbox():
|
||||||
"""Skrzynka odbiorcza"""
|
"""Skrzynka odbiorcza"""
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -52,6 +54,7 @@ def messages_inbox():
|
|||||||
|
|
||||||
@bp.route('/wiadomosci/wyslane')
|
@bp.route('/wiadomosci/wyslane')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_sent():
|
def messages_sent():
|
||||||
"""Wysłane wiadomości"""
|
"""Wysłane wiadomości"""
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -77,6 +80,7 @@ def messages_sent():
|
|||||||
|
|
||||||
@bp.route('/wiadomosci/nowa')
|
@bp.route('/wiadomosci/nowa')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_new():
|
def messages_new():
|
||||||
"""Formularz nowej wiadomości"""
|
"""Formularz nowej wiadomości"""
|
||||||
recipient_id = request.args.get('to', type=int)
|
recipient_id = request.args.get('to', type=int)
|
||||||
@ -124,6 +128,7 @@ def messages_new():
|
|||||||
|
|
||||||
@bp.route('/wiadomosci/wyslij', methods=['POST'])
|
@bp.route('/wiadomosci/wyslij', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_send():
|
def messages_send():
|
||||||
"""Wyślij wiadomość"""
|
"""Wyślij wiadomość"""
|
||||||
recipient_id = request.form.get('recipient_id', type=int)
|
recipient_id = request.form.get('recipient_id', type=int)
|
||||||
@ -171,6 +176,7 @@ def messages_send():
|
|||||||
|
|
||||||
@bp.route('/wiadomosci/<int:message_id>')
|
@bp.route('/wiadomosci/<int:message_id>')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_view(message_id):
|
def messages_view(message_id):
|
||||||
"""Czytaj wiadomość"""
|
"""Czytaj wiadomość"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -214,6 +220,7 @@ def messages_view(message_id):
|
|||||||
|
|
||||||
@bp.route('/wiadomosci/<int:message_id>/odpowiedz', methods=['POST'])
|
@bp.route('/wiadomosci/<int:message_id>/odpowiedz', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def messages_reply(message_id):
|
def messages_reply(message_id):
|
||||||
"""Odpowiedz na wiadomość"""
|
"""Odpowiedz na wiadomość"""
|
||||||
content = request.form.get('content', '').strip()
|
content = request.form.get('content', '').strip()
|
||||||
@ -262,6 +269,7 @@ def messages_reply(message_id):
|
|||||||
|
|
||||||
@bp.route('/api/messages/unread-count')
|
@bp.route('/api/messages/unread-count')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def api_unread_count():
|
def api_unread_count():
|
||||||
"""API: Liczba nieprzeczytanych wiadomości"""
|
"""API: Liczba nieprzeczytanych wiadomości"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -281,6 +289,7 @@ def api_unread_count():
|
|||||||
|
|
||||||
@bp.route('/api/notifications')
|
@bp.route('/api/notifications')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def api_notifications():
|
def api_notifications():
|
||||||
"""API: Get user notifications"""
|
"""API: Get user notifications"""
|
||||||
limit = request.args.get('limit', 20, type=int)
|
limit = request.args.get('limit', 20, type=int)
|
||||||
@ -330,6 +339,7 @@ def api_notifications():
|
|||||||
|
|
||||||
@bp.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
@bp.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def api_notification_mark_read(notification_id):
|
def api_notification_mark_read(notification_id):
|
||||||
"""API: Mark notification as read"""
|
"""API: Mark notification as read"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -355,6 +365,7 @@ def api_notification_mark_read(notification_id):
|
|||||||
|
|
||||||
@bp.route('/api/notifications/read-all', methods=['POST'])
|
@bp.route('/api/notifications/read-all', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def api_notifications_mark_all_read():
|
def api_notifications_mark_all_read():
|
||||||
"""API: Mark all notifications as read"""
|
"""API: Mark all notifications as read"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -379,6 +390,7 @@ def api_notifications_mark_all_read():
|
|||||||
|
|
||||||
@bp.route('/api/notifications/unread-count')
|
@bp.route('/api/notifications/unread-count')
|
||||||
@login_required
|
@login_required
|
||||||
|
@member_required
|
||||||
def api_notifications_unread_count():
|
def api_notifications_unread_count():
|
||||||
"""API: Get unread notifications count"""
|
"""API: Get unread notifications count"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|||||||
169
templates/chat_members_only.html
Normal file
169
templates/chat_members_only.html
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}NordaGPT - Dla Członków Izby{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.members-only-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 60px auto;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-only-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-only-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--norda-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-only-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin: 40px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
background: linear-gradient(135deg, var(--norda-primary) 0%, #1a5a8a 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
margin-top: 40px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text {
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
color: var(--norda-primary);
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
color: var(--norda-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: block;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="members-only-container">
|
||||||
|
<span class="members-only-icon">🤖</span>
|
||||||
|
|
||||||
|
<h1 class="members-only-title">NordaGPT - Asystent AI dla Członków</h1>
|
||||||
|
|
||||||
|
<p class="members-only-subtitle">
|
||||||
|
NordaGPT to inteligentny asystent, który zna wszystkie firmy członkowskie Izby NORDA.<br>
|
||||||
|
Ta funkcja jest dostępna wyłącznie dla członków stowarzyszenia.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card-icon">🔍</div>
|
||||||
|
<div class="feature-card-title">Wyszukiwanie firm</div>
|
||||||
|
<div class="feature-card-desc">
|
||||||
|
Znajdź partnerów biznesowych po usługach, kompetencjach lub lokalizacji
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card-icon">💬</div>
|
||||||
|
<div class="feature-card-title">Naturalna rozmowa</div>
|
||||||
|
<div class="feature-card-desc">
|
||||||
|
Zadawaj pytania w języku naturalnym - AI zrozumie Twoje potrzeby
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card-icon">📊</div>
|
||||||
|
<div class="feature-card-title">Pełna baza wiedzy</div>
|
||||||
|
<div class="feature-card-desc">
|
||||||
|
Dostęp do aktualności, wydarzeń, ogłoszeń B2B i dyskusji na forum
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card-icon">🤝</div>
|
||||||
|
<div class="feature-card-title">Rekomendacje</div>
|
||||||
|
<div class="feature-card-desc">
|
||||||
|
Poznaj opinie innych członków o firmach i ich usługach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cta-section">
|
||||||
|
<div class="cta-title">Chcesz korzystać z NordaGPT?</div>
|
||||||
|
<p class="cta-text">
|
||||||
|
Dołącz do Izby Przedsiębiorców NORDA i zyskaj dostęp do asystenta AI oraz wszystkich korzyści członkostwa.
|
||||||
|
</p>
|
||||||
|
<a href="https://norda-biznes.info/dolacz" class="cta-button" target="_blank">
|
||||||
|
Dowiedz się więcej o członkostwie
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('public.index') }}" class="back-link">
|
||||||
|
← Wróć do katalogu firm
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in New Issue
Block a user