From a237db339a475db9df82fb21e9e5e4f6d22d8e42 Mon Sep 17 00:00:00 2001 From: Josako Date: Tue, 13 Aug 2024 14:31:29 +0200 Subject: [PATCH] - Improved CSRF handling - Wordpress plugin for Evie Chat --- common/utils/middleware.py | 7 +- config/config.py | 12 +- config/logging_config.py | 4 +- eveai_app/__init__.py | 10 + eveai_app/errors.py | 9 + eveai_app/templates/error/generic.html | 9 + eveai_app/templates/security/login_user.html | 7 + eveai_app/views/basic_views.py | 12 + eveai_app/views/security_views.py | 51 ++- .../css/eveai-chat-style.css | 256 +++++++++++ .../eveai-chat-widget/eveai-chat_plugin.php | 118 +++++ .../eveai-chat-widget/js/eveai-chat-widget.js | 402 ++++++++++++++++++ .../eveai-chat-widget/js/eveai-sdk.js | 27 ++ .../Wordpress/eveai-chat-widget/readme.txt | 43 ++ 14 files changed, 944 insertions(+), 23 deletions(-) create mode 100644 eveai_app/templates/error/generic.html create mode 100644 integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css create mode 100644 integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php create mode 100644 integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js create mode 100644 integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js create mode 100644 integrations/Wordpress/eveai-chat-widget/readme.txt diff --git a/common/utils/middleware.py b/common/utils/middleware.py index d1c3503..e14a497 100644 --- a/common/utils/middleware.py +++ b/common/utils/middleware.py @@ -4,7 +4,8 @@ for handling tenant requests """ from flask_security import current_user -from flask import session, current_app +from flask import session, current_app, redirect +from common.utils.nginx_utils import prefixed_url_for from .database import Database @@ -15,6 +16,10 @@ def mw_before_request(): switch tenant schema """ + if 'tenant' not in session: + current_app.logger.warning('No tenant defined in session') + return redirect(prefixed_url_for('security_bp.login')) + tenant_id = session['tenant']['id'] if not tenant_id: raise Exception('Cannot switch schema for tenant: no tenant defined in session') diff --git a/config/config.py b/config/config.py index b7e15b2..d37db88 100644 --- a/config/config.py +++ b/config/config.py @@ -13,8 +13,11 @@ class Config(object): SECRET_KEY = environ.get('SECRET_KEY') SESSION_COOKIE_SECURE = False SESSION_COOKIE_HTTPONLY = True + SESSION_KEY_PREFIX = f'{environ.get('COMPONENT_NAME')}_' WTF_CSRF_ENABLED = True + WTF_CSRF_TIME_LIMIT = None + WTF_CSRF_SSL_STRICT = False # Set to True if using HTTPS # flask-security-too settings # SECURITY_URL_PREFIX = '/admin' @@ -31,7 +34,7 @@ class Config(object): # SECURITY_BLUEPRINT_NAME = 'security_bp' SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT') REMEMBER_COOKIE_SAMESITE = 'strict' - SESSION_COOKIE_SAMESITE = 'strict' + SESSION_COOKIE_SAMESITE = 'Lax' SECURITY_CONFIRMABLE = True SECURITY_TRACKABLE = True SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn' @@ -93,7 +96,7 @@ class Config(object): # Session Settings SESSION_TYPE = 'redis' - SESSION_PERMANENT = False + SESSION_PERMANENT = True SESSION_USE_SIGNER = True PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) SESSION_REFRESH_EACH_REQUEST = True @@ -200,6 +203,11 @@ class ProdConfig(Config): FLASK_DEBUG = False EXPLAIN_TEMPLATE_LOADING = False + # SESSION SETTINGS + SESSION_COOKIE_SECURE = True + + WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS + # Database Settings DB_HOST = environ.get('DB_HOST') DB_USER = environ.get('DB_USER') diff --git a/config/logging_config.py b/config/logging_config.py index 00ac088..790351e 100644 --- a/config/logging_config.py +++ b/config/logging_config.py @@ -117,11 +117,11 @@ LOGGING = { 'formatters': { 'standard': { 'format': '%(asctime)s [%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] ' - '[Thread: %(threadName)s] [Host: %(hostname)s]: %(message)s' + '[Thread: %(threadName)s]: %(message)s' }, 'graylog': { 'format': '[%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] ' - '[Thread: %(threadName)s] [Host: %(hostname)s]: %(message)s', + '[Thread: %(threadName)s]: %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', }, }, diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index ed90506..630f8d9 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -53,6 +53,10 @@ def create_app(config_file=None): register_extensions(app) + # Configure CSRF protection + app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection + app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens + app.celery = make_celery(app.name, app.config) init_celery(app.celery, app) @@ -88,6 +92,12 @@ def create_app(config_file=None): } return jsonify(response), 500 + @app.before_request + def before_request(): + # app.logger.debug(f"Before request - Session ID: {session.sid}") + app.logger.debug(f"Before request - Session data: {session}") + app.logger.debug(f"Before request - Request headers: {request.headers}") + # Register API register_api(app) diff --git a/eveai_app/errors.py b/eveai_app/errors.py index 2d9279e..4c6f0ec 100644 --- a/eveai_app/errors.py +++ b/eveai_app/errors.py @@ -27,9 +27,18 @@ def access_forbidden(error): return render_template('error/403.html') +def key_error_handler(error): + # Check if the KeyError is specifically for 'tenant' + if str(error) == "'tenant'": + return redirect(prefixed_url_for('security.login')) + # For other KeyErrors, you might want to log the error and return a generic error page + return render_template('error/generic.html', error_message="An unexpected error occurred"), 500 + + def register_error_handlers(app): app.register_error_handler(404, not_found_error) app.register_error_handler(500, internal_server_error) app.register_error_handler(401, not_authorised_error) app.register_error_handler(403, not_authorised_error) + app.register_error_handler(KeyError, key_error_handler) diff --git a/eveai_app/templates/error/generic.html b/eveai_app/templates/error/generic.html new file mode 100644 index 0000000..2c7bb71 --- /dev/null +++ b/eveai_app/templates/error/generic.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}Unexpected Error{% endblock %} + +{% block content_title %}Internal Server error{% endblock %} +{% block content_description %}Something unexpected happened! The administrator has been notified.{% endblock %} +{% block content %} +

Return home

+{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/security/login_user.html b/eveai_app/templates/security/login_user.html index 7433c94..cf9e117 100644 --- a/eveai_app/templates/security/login_user.html +++ b/eveai_app/templates/security/login_user.html @@ -20,6 +20,13 @@ {#

#}

{{ login_user_form.submit() }}

+ + +{#
#} +{#

Debugging Information:

#} +{#

CSRF Token: {{ login_user_form.csrf_token.current_token }}

#} +{#

Session ID: {{ session.sid }}

#} +{#
#} {% endblock %} {% block content_footer %} First time here? Forgot your password? diff --git a/eveai_app/views/basic_views.py b/eveai_app/views/basic_views.py index 979c1cf..ca15454 100644 --- a/eveai_app/views/basic_views.py +++ b/eveai_app/views/basic_views.py @@ -1,5 +1,6 @@ from flask import request, render_template, Blueprint, session, current_app, jsonify from flask_security import roles_required, roles_accepted +from flask_wtf.csrf import generate_csrf from .basic_forms import SessionDefaultsForm @@ -59,3 +60,14 @@ def set_user_timezone(): def health(): return jsonify({'status': 'ok'}), 200 + +@basic_bp.route('/check_csrf', methods=['GET']) +def check_csrf(): + csrf_token = generate_csrf() + return jsonify({ + 'csrf_token_in_session': session.get('csrf_token'), + 'generated_csrf_token': csrf_token, + 'session_id': session.sid if hasattr(session, 'sid') else None, + 'session_data': dict(session) + }) + diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py index 23c965d..e0ae75f 100644 --- a/eveai_app/views/security_views.py +++ b/eveai_app/views/security_views.py @@ -3,6 +3,7 @@ from flask import Blueprint, render_template, redirect, request, flash, current_ from flask_security import current_user, login_required, login_user, logout_user from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password from flask_security.forms import LoginForm +from flask_wtf.csrf import CSRFError, generate_csrf from urllib.parse import urlparse from datetime import datetime as dt, timezone as tz @@ -45,27 +46,41 @@ def login(): form = LoginForm() - if form.validate_on_submit(): - current_app.logger.debug(f'Validating login form: {form.email.data}') - user = User.query.filter_by(email=form.email.data).first() - if user is None or not verify_and_update_password(form.password.data, user): - flash('Invalid username or password', 'danger') + if request.method == 'POST': + current_app.logger.debug(f"Starting login procedure for {form.email.data}") + try: + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user is None or not verify_and_update_password(form.password.data, user): + flash('Invalid username or password', 'danger') + current_app.logger.debug(f'Failed to login user') + return redirect(prefixed_url_for('security_bp.login')) + + if login_user(user): + current_app.logger.info(f'Login successful! Current User is {current_user.email}') + db.session.commit() + if current_user.has_roles('Super User'): + return redirect(prefixed_url_for('user_bp.select_tenant')) + else: + return redirect(prefixed_url_for('user_bp.tenant_overview')) + else: + flash('Invalid username or password', 'danger') + current_app.logger.debug(f'Failed to login user {user.email}') + abort(401) + else: + current_app.logger.debug(f'Invalid login form: {form.errors}') + + except CSRFError: + current_app.logger.warning('CSRF token mismatch during login attempt') + flash('Your session has expired. Please try logging in again.', 'danger') return redirect(prefixed_url_for('security_bp.login')) - if login_user(user): - current_app.logger.info(f'Login successful! Current User is {current_user.email}') - db.session.commit() - if current_user.has_roles('Super User'): - return redirect(prefixed_url_for('user_bp.select_tenant')) - else: - return redirect(prefixed_url_for('user_bp.tenant_overview')) - else: - flash('Invalid username or password', 'danger') - current_app.logger.debug(f'Failed to login user {user.email}') - abort(401) - else: - current_app.logger.debug(f'Invalid login form: {form.errors}') + if request.method == 'GET': + csrf_token = generate_csrf() + current_app.logger.debug(f'Generated new CSRF token: {csrf_token}') + # current_app.logger.debug(f"Login route completed - Session ID: {session.sid}") + current_app.logger.debug(f"Login route completed - Session data: {session}") return render_template('security/login_user.html', login_user_form=form) diff --git a/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css b/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css new file mode 100644 index 0000000..ba3fae8 --- /dev/null +++ b/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css @@ -0,0 +1,256 @@ +/* eveai_chat.css */ +:root { + --user-message-bg: #292929; /* Default user message background color */ + --bot-message-bg: #1e1e1e; /* Default bot message background color */ + --chat-bg: #1e1e1e; /* Default chat background color */ + --status-line-color: #e9e9e9; /* Color for the status line text */ + --status-line-bg: #1e1e1e; /* Background color for the status line */ + --status-line-height: 30px; /* Fixed height for the status line */ + + --algorithm-color-rag-tenant: #0f0; /* Green for RAG_TENANT */ + --algorithm-color-rag-wikipedia: #00f; /* Blue for RAG_WIKIPEDIA */ + --algorithm-color-rag-google: #ff0; /* Yellow for RAG_GOOGLE */ + --algorithm-color-llm: #800080; /* Purple for RAG_LLM */ + + /*--font-family: 'Arial, sans-serif'; !* Default font family *!*/ + --font-family: 'ui-sans-serif, -apple-system, system-ui, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, Helvetica, Apple Color Emoji, Arial, Segoe UI Emoji, Segoe UI Symbol'; + --font-color: #e9e9e9; /* Default font color */ + --user-message-font-color: #e9e9e9; /* User message font color */ + --bot-message-font-color: #e9e9e9; /* Bot message font color */ + --input-bg: #292929; /* Input background color */ + --input-border: #ccc; /* Input border color */ + --input-text-color: #e9e9e9; /* Input text color */ + --button-color: #007bff; /* Button text color */ + + /* Variables for hyperlink backgrounds */ + --link-bg: #1e1e1e; /* Default background color for hyperlinks */ + --link-hover-bg: #1e1e1e; /* Background color on hover for hyperlinks */ + --link-color: #dec981; /* Default text color for hyperlinks */ + --link-hover-color: #D68F53; /* Text color on hover for hyperlinks */ + + /* New scrollbar variables */ + --scrollbar-bg: #292929; /* Background color for the scrollbar track */ + --scrollbar-thumb: #4b4b4b; /* Color for the scrollbar thumb */ + --scrollbar-thumb-hover: #dec981; /* Color for the thumb on hover */ + --scrollbar-thumb-active: #D68F53; /* Color for the thumb when active (dragged) */ + + /* Thumb colors */ + --thumb-icon-outlined: #4b4b4b; + --thumb-icon-filled: #e9e9e9; + + /* Connection Status colors */ + --status-connected-color: #28a745; /* Green color for connected status */ + --status-disconnected-color: #ffc107; /* Orange color for disconnected status */ +} + +/* Connection status styles */ +.connection-status-icon { + vertical-align: middle; + font-size: 24px; + margin-right: 8px; +} + +.status-connected { + color: var(--status-connected-color); +} + +.status-disconnected { + color: var(--status-disconnected-color); +} + +/* Custom scrollbar styles */ +.messages-area::-webkit-scrollbar { + width: 12px; /* Width of the scrollbar */ +} + +.messages-area::-webkit-scrollbar-track { + background: var(--scrollbar-bg); /* Background color for the track */ +} + +.messages-area::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); /* Color of the thumb */ + border-radius: 10px; /* Rounded corners for the thumb */ + border: 3px solid var(--scrollbar-bg); /* Space around the thumb */ +} + +.messages-area::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover); /* Color when hovering over the thumb */ +} + +.messages-area::-webkit-scrollbar-thumb:active { + background-color: var(--scrollbar-thumb-active); /* Color when active (dragging) */ +} + +/* For Firefox */ +.messages-area { + scrollbar-width: thin; /* Make scrollbar thinner */ + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg); /* Thumb and track colors */ +} + +/* General Styles */ +.chat-container { + display: flex; + flex-direction: column; + height: 99vh; + /*max-height: 100vh;*/ + max-width: 600px; + margin: auto; + border: 1px solid #ccc; + border-radius: 8px; + overflow: hidden; + background-color: var(--chat-bg); + font-family: var(--font-family); /* Apply the default font family */ + color: var(--font-color); /* Apply the default font color */ +} + +.messages-area { + flex: 1; + overflow-y: auto; + padding: 10px; + background-color: var(--bot-message-bg); +} + +.message { + max-width: 90%; + margin-bottom: 10px; + padding: 10px; + border-radius: 15px; +} + +.message.user { + margin-left: auto; + background-color: var(--user-message-bg); + color: var(--user-message-font-color); /* Apply user message font color */ +} + +.message.bot { + background-color: var(--bot-message-bg); + color: var(--bot-message-font-color); /* Apply bot message font color */ +} + +.message-icons { + display: flex; + align-items: center; +} + +/* Scoped styles for thumb icons */ +.thumb-icon.outlined { + color: var(--thumb-icon-outlined); /* Color for outlined state */ +} + +.thumb-icon.filled { + color: var(--thumb-icon-filled); /* Color for filled state */ +} + +/* Default styles for material icons */ +.material-icons { + font-size: 24px; + vertical-align: middle; + cursor: pointer; +} + +.question-area { + padding: 10px; + display: flex; + align-items: center; + background-color: var(--user-message-bg); +} + +.question-area input { + flex: 1; + border: none; + padding: 10px; + border-radius: 15px; + margin-right: 10px; + background-color: var(--input-bg); /* Apply input background color */ + border: 1px solid var(--input-border); /* Apply input border color */ + color: var(--input-text-color); /* Apply input text color */ +} + +.question-area button { + background: none; + border: none; + cursor: pointer; + color: var(--button-color); +} + +/* Styles for the send icon */ +.send-icon { + font-size: 24px; /* Size of the icon */ + color: var(--button-color); /* Color of the icon */ +} + +.send-icon.disabled { + color: grey; /* Color for the disabled state */ + cursor: not-allowed; /* Change cursor to indicate disabled state */ +} + +/* New CSS for the status-line */ +.status-line { + height: var(--status-line-height); /* Fixed height for the status line */ + padding: 5px 10px; + background-color: var(--status-line-bg); /* Background color */ + color: var(--status-line-color); /* Text color */ + font-size: 0.9rem; /* Slightly smaller font size */ + text-align: center; /* Centered text */ + border-top: 1px solid #ccc; /* Subtle top border */ + display: flex; + align-items: center; + justify-content: flex-start; +} + +/* Algorithm-specific colors for fingerprint icon */ +.fingerprint-rag-tenant { + color: var(--algorithm-color-rag-tenant); +} + +.fingerprint-rag-wikipedia { + color: var(--algorithm-color-rag-wikipedia); +} + +.fingerprint-rag-google { + color: var(--algorithm-color-rag-google); +} + +.fingerprint-llm { + color: var(--algorithm-color-llm); +} + +/* Styling for citation links */ +.citations a { + background-color: var(--link-bg); /* Apply default background color */ + color: var(--link-color); /* Apply default link color */ + padding: 2px 4px; /* Add padding for better appearance */ + border-radius: 3px; /* Add slight rounding for a modern look */ + text-decoration: none; /* Remove default underline */ + transition: background-color 0.3s, color 0.3s; /* Smooth transition for hover effects */ +} + +.citations a:hover { + background-color: var(--link-hover-bg); /* Background color on hover */ + color: var(--link-hover-color); /* Text color on hover */ +} + +/* Media queries for responsiveness */ +@media (max-width: 768px) { + .chat-container { + max-width: 90%; /* Reduce max width on smaller screens */ + } +} + +@media (max-width: 480px) { + .chat-container { + max-width: 95%; /* Further reduce max width on very small screens */ + } + + .question-area input { + font-size: 0.9rem; /* Adjust input font size for smaller screens */ + } + + .status-line { + font-size: 0.8rem; /* Adjust status line font size for smaller screens */ + } +} + + + diff --git a/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php b/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php new file mode 100644 index 0000000..3f866d7 --- /dev/null +++ b/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php @@ -0,0 +1,118 @@ +"; + $output .= ""; + + return $output; +} +add_shortcode('eveai_chat', 'eveai_chat_shortcode'); + +// Add admin menu +function eveai_chat_admin_menu() { + add_options_page('EveAI Chat Settings', 'EveAI Chat', 'manage_options', 'eveai-chat-settings', 'eveai_chat_settings_page'); +} +add_action('admin_menu', 'eveai_chat_admin_menu'); + +// Settings page +function eveai_chat_settings_page() { + ?> +
+

EveAI Chat Settings

+
+ +
+
+ Enter your EveAI Chat configuration details below:

'; +} + +function eveai_chat_tenant_id_input() { + $options = get_option('eveai_chat_options'); + echo ""; +} + +function eveai_chat_api_key_input() { + $options = get_option('eveai_chat_options'); + echo ""; +} + +function eveai_chat_domain_input() { + $options = get_option('eveai_chat_options'); + echo ""; +} + +function eveai_chat_language_input() { + $options = get_option('eveai_chat_options'); + echo ""; +} + +function eveai_chat_options_validate($input) { + $new_input = array(); + $new_input['tenant_id'] = sanitize_text_field($input['tenant_id']); + $new_input['api_key'] = sanitize_text_field($input['api_key']); + $new_input['domain'] = esc_url_raw($input['domain']); + $new_input['language'] = sanitize_text_field($input['language']); + return $new_input; +} \ No newline at end of file diff --git a/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js b/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js new file mode 100644 index 0000000..8878906 --- /dev/null +++ b/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js @@ -0,0 +1,402 @@ +class EveAIChatWidget extends HTMLElement { + static get observedAttributes() { + return ['tenant-id', 'api-key', 'domain', 'language']; + } + + constructor() { + super(); + this.socket = null; // Initialize socket to null + this.attributesSet = false; // Flag to check if all attributes are set + this.jwtToken = null; // Initialize jwtToken to null + this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone + this.heartbeatInterval = null; + this.idleTime = 0; // in milliseconds + this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hours in milliseconds + console.log('EveAIChatWidget constructor called'); + } + + connectedCallback() { + console.log('connectedCallback called'); + this.innerHTML = this.getTemplate(); + this.messagesArea = this.querySelector('.messages-area'); + this.questionInput = this.querySelector('.question-area input'); + this.sendButton = this.querySelector('.send-icon'); + this.statusLine = this.querySelector('.status-line'); + this.statusMessage = this.querySelector('.status-message'); + this.connectionStatusIcon = this.querySelector('.connection-status-icon'); + + this.sendButton.addEventListener('click', () => this.handleSendMessage()); + this.questionInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + this.handleSendMessage(); + } + }); + + if (this.areAllAttributesSet() && !this.socket) { + console.log('Attributes already set in connectedCallback, initializing socket'); + this.initializeSocket(); + } + } + + attributeChangedCallback(name, oldValue, newValue) { + console.log(`attributeChangedCallback called: ${name} changed from ${oldValue} to ${newValue}`); + this.updateAttributes(); + + if (this.areAllAttributesSet() && !this.socket) { + console.log('All attributes set in attributeChangedCallback, initializing socket'); + this.attributesSet = true; + this.initializeSocket(); + } + } + + updateAttributes() { + this.tenantId = this.getAttribute('tenant-id'); + this.apiKey = this.getAttribute('api-key'); + this.domain = this.getAttribute('domain'); + this.language = this.getAttribute('language'); + console.log('Updated attributes:', { + tenantId: this.tenantId, + apiKey: this.apiKey, + domain: this.domain, + language: this.language + }); + } + + areAllAttributesSet() { + const tenantId = this.getAttribute('tenant-id'); + const apiKey = this.getAttribute('api-key'); + const domain = this.getAttribute('domain'); + const language = this.getAttribute('language'); + console.log('Checking if all attributes are set:', { + tenantId, + apiKey, + domain, + language + }); + return tenantId && apiKey && domain && language; + } + + initializeSocket() { + if (this.socket) { + console.log('Socket already initialized'); + return; + } + if (!this.domain || this.domain === 'null') { + console.error('Domain attribute is missing or invalid'); + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); + return; + } + console.log(`Initializing socket connection to ${this.domain}`); + + // Ensure apiKey is passed in the query parameters + this.socket = io(this.domain, { + path: '/chat/socket.io/', + transports: ['websocket', 'polling'], + query: { + tenantId: this.tenantId, + apiKey: this.apiKey // Ensure apiKey is included here + }, + auth: { + token: 'Bearer ' + this.apiKey // Ensure token is included here + }, + reconnectionAttempts: Infinity, // Infinite reconnection attempts + reconnectionDelay: 5000, // Delay between reconnections + timeout: 20000 // Connection timeout + }); + + console.log(`Finished initializing socket connection to ${this.domain}`); + + this.socket.on('connect', (data) => { + console.log('Socket connected OK'); + this.setStatusMessage('Connected to EveAI.'); + this.updateConnectionStatus(true); + this.startHeartbeat(); + }); + + this.socket.on('authenticated', (data) => { + console.log('Authenticated event received: ', data); + this.setStatusMessage('Authenticated.'); + if (data.token) { + this.jwtToken = data.token; // Store the JWT token received from the server + } + }); + + this.socket.on('connect_error', (err) => { + console.error('Socket connection error:', err); + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); + this.updateConnectionStatus(false); + }); + + this.socket.on('connect_timeout', () => { + console.error('Socket connection timeout'); + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); + this.updateConnectionStatus(false); + }); + + this.socket.on('disconnect', (reason) => { + console.log('Socket disconnected: ', reason); + if (reason === 'io server disconnect') { + // Server disconnected the socket + this.socket.connect(); // Attempt to reconnect + } + this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.'); + this.updateConnectionStatus(false); + this.stopHeartbeat(); + }); + + this.socket.on('reconnect_attempt', () => { + console.log('Attempting to reconnect to the server...'); + this.setStatusMessage('Attempting to reconnect...'); + }); + + this.socket.on('reconnect', () => { + console.log('Successfully reconnected to the server'); + this.setStatusMessage('Reconnected to EveAI.'); + this.updateConnectionStatus(true); + this.startHeartbeat(); + }); + + this.socket.on('bot_response', (data) => { + if (data.tenantId === this.tenantId) { + console.log('Initial response received:', data); + console.log('Task ID received:', data.taskId); + this.checkTaskStatus(data.taskId); + this.setStatusMessage('Processing...'); + } + }); + + this.socket.on('task_status', (data) => { + console.log('Task status received:', data.status); + console.log('Task ID received:', data.taskId); + console.log('Citations type:', typeof data.citations, 'Citations:', data.citations); + + if (data.status === 'pending') { + this.updateProgress(); + setTimeout(() => this.checkTaskStatus(data.taskId), 1000); // Poll every second + } else if (data.status === 'success') { + this.addBotMessage(data.answer, data.interaction_id, data.algorithm, data.citations); + this.clearProgress(); // Clear progress indicator when done + } else { + this.setStatusMessage('Failed to process message.'); + } + }); + } + + setStatusMessage(message) { + this.statusMessage.textContent = message; + } + + updateConnectionStatus(isConnected) { + if (isConnected) { + this.connectionStatusIcon.textContent = 'link'; + this.connectionStatusIcon.classList.remove('status-disconnected'); + this.connectionStatusIcon.classList.add('status-connected'); + } else { + this.connectionStatusIcon.textContent = 'link_off'; + this.connectionStatusIcon.classList.remove('status-connected'); + this.connectionStatusIcon.classList.add('status-disconnected'); + } + } + + startHeartbeat() { + this.stopHeartbeat(); // Clear any existing interval + this.heartbeatInterval = setInterval(() => { + if (this.socket && this.socket.connected) { + this.socket.emit('heartbeat'); + this.idleTime += 30000; + if (this.idleTime >= this.maxConnectionIdleTime) { + this.socket.disconnect(); + this.setStatusMessage('Disconnected due to inactivity.'); + this.updateConnectionStatus(false); + this.stopHeartbeat(); + } + } + }, 30000); // Send a heartbeat every 30 seconds + } + + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + } + + updateProgress() { + if (!this.statusMessage.textContent) { + this.statusMessage.textContent = 'Processing...'; + } else { + this.statusMessage.textContent += '.'; // Append a dot + } + } + + clearProgress() { + this.statusMessage.textContent = ''; + this.toggleSendButton(false); // Re-enable and revert send button to outlined version + } + + checkTaskStatus(taskId) { + this.updateProgress(); + this.socket.emit('check_task_status', { task_id: taskId }); + } + + getTemplate() { + return ` +
+
+
+ + send +
+
+ link_off + +
+
+ `; + } + + addUserMessage(text) { + const message = document.createElement('div'); + message.classList.add('message', 'user'); + message.innerHTML = `

${text}

`; + this.messagesArea.appendChild(message); + this.messagesArea.scrollTop = this.messagesArea.scrollHeight; + } + + handleFeedback(feedback, interactionId) { + // Send feedback to the backend + console.log('handleFeedback called'); + if (!this.socket) { + console.error('Socket is not initialized'); + return; + } + if (!this.jwtToken) { + console.error('JWT token is not available'); + return; + } + console.log('Sending message to backend'); + console.log(`Feedback for ${interactionId}: ${feedback}`); + this.socket.emit('feedback', { tenantId: this.tenantId, token: this.jwtToken, feedback, interactionId }); + this.setStatusMessage('Feedback sent.'); + } + + addBotMessage(text, interactionId, algorithm = 'default', citations = []) { + const message = document.createElement('div'); + message.classList.add('message', 'bot'); + + let content = marked.parse(text); + let citationsHtml = citations.map(url => `${url}`).join('
'); + + let algorithmClass; + switch (algorithm) { + case 'RAG_TENANT': + algorithmClass = 'fingerprint-rag-tenant'; + break; + case 'RAG_WIKIPEDIA': + algorithmClass = 'fingerprint-rag-wikipedia'; + break; + case 'RAG_GOOGLE': + algorithmClass = 'fingerprint-rag-google'; + break; + case 'LLM': + algorithmClass = 'fingerprint-llm'; + break; + default: + algorithmClass = ''; + } + + message.innerHTML = ` +

${content}

+ ${citationsHtml ? `

${citationsHtml}

` : ''} +
+ fingerprint + thumb_up_off_alt + thumb_down_off_alt +
+ `; + this.messagesArea.appendChild(message); + + // Add event listeners for feedback buttons + const thumbsUp = message.querySelector('i[data-feedback="up"]'); + const thumbsDown = message.querySelector('i[data-feedback="down"]'); + thumbsUp.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'up', interactionId)); + thumbsDown.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'down', interactionId)); + + this.messagesArea.scrollTop = this.messagesArea.scrollHeight; + } + +toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) { + console.log('feedback called'); + this.idleTime = 0; // Reset idle time + if (feedback === 'up') { + thumbsUp.textContent = 'thumb_up'; // Change to filled icon + thumbsUp.classList.remove('outlined'); + thumbsUp.classList.add('filled'); + thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined + thumbsDown.classList.add('outlined'); + thumbsDown.classList.remove('filled'); + } else { + thumbsDown.textContent = 'thumb_down'; // Change to filled icon + thumbsDown.classList.remove('outlined'); + thumbsDown.classList.add('filled'); + thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined + thumbsUp.classList.add('outlined'); + thumbsUp.classList.remove('filled'); + } + + // Send feedback to the backend + this.handleFeedback(feedback, interactionId); + } + + handleSendMessage() { + console.log('handleSendMessage called'); + this.idleTime = 0; // Reset idle time + const message = this.questionInput.value.trim(); + if (message) { + this.addUserMessage(message); + this.questionInput.value = ''; + this.sendMessageToBackend(message); + this.toggleSendButton(true); // Disable and change send button to filled version + } + } + + sendMessageToBackend(message) { + console.log('sendMessageToBackend called'); + if (!this.socket) { + console.error('Socket is not initialized'); + return; + } + if (!this.jwtToken) { + console.error('JWT token is not available'); + return; + } + console.log('Sending message to backend'); + this.socket.emit('user_message', { + tenantId: this.tenantId, + token: this.jwtToken, + message, + language: this.language, + timezone: this.userTimezone + }); + this.setStatusMessage('Processing started ...') + } + + toggleSendButton(isProcessing) { + if (isProcessing) { + this.sendButton.textContent = 'send'; // Filled send icon + this.sendButton.classList.remove('outlined'); + this.sendButton.classList.add('filled'); + this.sendButton.classList.add('disabled'); // Add disabled class for styling + this.sendButton.style.pointerEvents = 'none'; // Disable click events + } else { + this.sendButton.textContent = 'send'; // Outlined send icon + this.sendButton.classList.add('outlined'); + this.sendButton.classList.remove('filled'); + this.sendButton.classList.remove('disabled'); // Remove disabled class + this.sendButton.style.pointerEvents = 'auto'; // Re-enable click events + } + } +} + +customElements.define('eveai-chat-widget', EveAIChatWidget); + diff --git a/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js b/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js new file mode 100644 index 0000000..9e53d41 --- /dev/null +++ b/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js @@ -0,0 +1,27 @@ +// static/js/eveai-sdk.js +class EveAI { + constructor(tenantId, apiKey, domain, language) { + this.tenantId = tenantId; + this.apiKey = apiKey; + this.domain = domain; + this.language = language; + + console.log('EveAI constructor:', { tenantId, apiKey, domain }); + } + + initializeChat(containerId) { + const container = document.getElementById(containerId); + if (container) { + container.innerHTML = ''; + customElements.whenDefined('eveai-chat-widget').then(() => { + const chatWidget = container.querySelector('eveai-chat-widget'); + chatWidget.setAttribute('tenant-id', this.tenantId); + chatWidget.setAttribute('api-key', this.apiKey); + chatWidget.setAttribute('domain', this.domain); + chatWidget.setAttribute('language', this.language); + }); + } else { + console.error('Container not found'); + } + } +} \ No newline at end of file diff --git a/integrations/Wordpress/eveai-chat-widget/readme.txt b/integrations/Wordpress/eveai-chat-widget/readme.txt new file mode 100644 index 0000000..4671f11 --- /dev/null +++ b/integrations/Wordpress/eveai-chat-widget/readme.txt @@ -0,0 +1,43 @@ +=== EveAI Chat Widget === +Contributors: Josako +Tags: chat, ai +Requires at least: 5.0 +Tested up to: 5.9 +Stable tag: 1.2 +License: GPLv2 or later +License URI: http://www.gnu.org/licenses/gpl-2.0.html + +Integrates the EveAI chat interface into your WordPress site. + +== Description == + +This plugin allows you to easily add the EveAI chat widget to your WordPress site. It provides a configurable interface to set up your EveAI chat parameters. + +== Installation == + +1. Upload the `eveai-chat-widget` folder to the `/wp-content/plugins/` directory +2. Activate the plugin through the 'Plugins' menu in WordPress +3. Go to Settings > EveAI Chat to configure your chat widget parameters + +== Frequently Asked Questions == + += Where do I get my EveAI credentials? = + +Contact your EveAI service provider to obtain your Tenant ID, API Key, and Domain. + +== Changelog == + += 1.2 = +* Create shortcodes + += 1.1 = +* Added configurable settings +* Improved security with server-side API key handling + += 1.0 = +* Initial release + +== Upgrade Notice == + += 1.1 = +This version adds configurable settings and improves security. Please update your EveAI credentials after upgrading. \ No newline at end of file