- Improved CSRF handling

- Wordpress plugin for Evie Chat
This commit is contained in:
Josako
2024-08-13 14:31:29 +02:00
parent ab38dd7540
commit a237db339a
14 changed files with 944 additions and 23 deletions

View File

@@ -4,7 +4,8 @@ for handling tenant requests
""" """
from flask_security import current_user 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 from .database import Database
@@ -15,6 +16,10 @@ def mw_before_request():
switch tenant schema 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'] tenant_id = session['tenant']['id']
if not tenant_id: if not tenant_id:
raise Exception('Cannot switch schema for tenant: no tenant defined in session') raise Exception('Cannot switch schema for tenant: no tenant defined in session')

View File

@@ -13,8 +13,11 @@ class Config(object):
SECRET_KEY = environ.get('SECRET_KEY') SECRET_KEY = environ.get('SECRET_KEY')
SESSION_COOKIE_SECURE = False SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
SESSION_KEY_PREFIX = f'{environ.get('COMPONENT_NAME')}_'
WTF_CSRF_ENABLED = True WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = None
WTF_CSRF_SSL_STRICT = False # Set to True if using HTTPS
# flask-security-too settings # flask-security-too settings
# SECURITY_URL_PREFIX = '/admin' # SECURITY_URL_PREFIX = '/admin'
@@ -31,7 +34,7 @@ class Config(object):
# SECURITY_BLUEPRINT_NAME = 'security_bp' # SECURITY_BLUEPRINT_NAME = 'security_bp'
SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT') SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT')
REMEMBER_COOKIE_SAMESITE = 'strict' REMEMBER_COOKIE_SAMESITE = 'strict'
SESSION_COOKIE_SAMESITE = 'strict' SESSION_COOKIE_SAMESITE = 'Lax'
SECURITY_CONFIRMABLE = True SECURITY_CONFIRMABLE = True
SECURITY_TRACKABLE = True SECURITY_TRACKABLE = True
SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn' SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn'
@@ -93,7 +96,7 @@ class Config(object):
# Session Settings # Session Settings
SESSION_TYPE = 'redis' SESSION_TYPE = 'redis'
SESSION_PERMANENT = False SESSION_PERMANENT = True
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) PERMANENT_SESSION_LIFETIME = timedelta(minutes=60)
SESSION_REFRESH_EACH_REQUEST = True SESSION_REFRESH_EACH_REQUEST = True
@@ -200,6 +203,11 @@ class ProdConfig(Config):
FLASK_DEBUG = False FLASK_DEBUG = False
EXPLAIN_TEMPLATE_LOADING = False EXPLAIN_TEMPLATE_LOADING = False
# SESSION SETTINGS
SESSION_COOKIE_SECURE = True
WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS
# Database Settings # Database Settings
DB_HOST = environ.get('DB_HOST') DB_HOST = environ.get('DB_HOST')
DB_USER = environ.get('DB_USER') DB_USER = environ.get('DB_USER')

View File

@@ -117,11 +117,11 @@ LOGGING = {
'formatters': { 'formatters': {
'standard': { 'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] ' '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': { 'graylog': {
'format': '[%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] ' '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', 'datefmt': '%Y-%m-%d %H:%M:%S',
}, },
}, },

View File

@@ -53,6 +53,10 @@ def create_app(config_file=None):
register_extensions(app) 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) app.celery = make_celery(app.name, app.config)
init_celery(app.celery, app) init_celery(app.celery, app)
@@ -88,6 +92,12 @@ def create_app(config_file=None):
} }
return jsonify(response), 500 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
register_api(app) register_api(app)

View File

@@ -27,9 +27,18 @@ def access_forbidden(error):
return render_template('error/403.html') 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): def register_error_handlers(app):
app.register_error_handler(404, not_found_error) app.register_error_handler(404, not_found_error)
app.register_error_handler(500, internal_server_error) app.register_error_handler(500, internal_server_error)
app.register_error_handler(401, not_authorised_error) app.register_error_handler(401, not_authorised_error)
app.register_error_handler(403, not_authorised_error) app.register_error_handler(403, not_authorised_error)
app.register_error_handler(KeyError, key_error_handler)

View File

@@ -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 %}
<p><a href="{{ url_for('basic_bp.index') }}">Return home</a></p>
{% endblock %}

View File

@@ -20,6 +20,13 @@
{# </p>#} {# </p>#}
<p>{{ login_user_form.submit() }}</p> <p>{{ login_user_form.submit() }}</p>
</form> </form>
<!-- Debugging information -->
{# <div style="margin-top: 20px; border: 1px solid #ccc; padding: 10px;">#}
{# <h4>Debugging Information:</h4>#}
{# <p>CSRF Token: {{ login_user_form.csrf_token.current_token }}</p>#}
{# <p>Session ID: {{ session.sid }}</p>#}
{# </div>#}
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}
First time here? Forgot your password? First time here? Forgot your password?

View File

@@ -1,5 +1,6 @@
from flask import request, render_template, Blueprint, session, current_app, jsonify from flask import request, render_template, Blueprint, session, current_app, jsonify
from flask_security import roles_required, roles_accepted from flask_security import roles_required, roles_accepted
from flask_wtf.csrf import generate_csrf
from .basic_forms import SessionDefaultsForm from .basic_forms import SessionDefaultsForm
@@ -59,3 +60,14 @@ def set_user_timezone():
def health(): def health():
return jsonify({'status': 'ok'}), 200 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)
})

View File

@@ -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 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.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
from flask_security.forms import LoginForm from flask_security.forms import LoginForm
from flask_wtf.csrf import CSRFError, generate_csrf
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
@@ -45,27 +46,41 @@ def login():
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if request.method == 'POST':
current_app.logger.debug(f'Validating login form: {form.email.data}') current_app.logger.debug(f"Starting login procedure for {form.email.data}")
user = User.query.filter_by(email=form.email.data).first() try:
if user is None or not verify_and_update_password(form.password.data, user): if form.validate_on_submit():
flash('Invalid username or password', 'danger') 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')) return redirect(prefixed_url_for('security_bp.login'))
if login_user(user): if request.method == 'GET':
current_app.logger.info(f'Login successful! Current User is {current_user.email}') csrf_token = generate_csrf()
db.session.commit() current_app.logger.debug(f'Generated new CSRF token: {csrf_token}')
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}')
# 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) return render_template('security/login_user.html', login_user_form=form)

View File

@@ -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 */
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
Plugin Name: EveAI Chat Widget
Plugin URI: https://askeveai.com/
Description: Integrates the EveAI chat interface into your WordPress site.
Version: 1.2
Author: Josako, Pieter Laroy
Author URI: https://askeveai.com/about/
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/
// Enqueue necessary scripts and styles
function eveai_chat_enqueue_scripts() {
wp_enqueue_script('socket-io', 'https://cdn.socket.io/4.0.1/socket.io.min.js', array(), '4.0.1', true);
wp_enqueue_script('marked', 'https://cdn.jsdelivr.net/npm/marked/marked.min.js', array(), '1.0.0', true);
wp_enqueue_script('eveai-sdk', plugin_dir_url(__FILE__) . 'js/eveai-sdk.js', array(), '1.0.0', true);
wp_enqueue_script('eveai-chat-widget', plugin_dir_url(__FILE__) . 'js/eveai-chat-widget.js', array('eveai-sdk'), '1.0.0', true);
wp_enqueue_style('material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons');
wp_enqueue_style('eveai-chat-style', plugin_dir_url(__FILE__) . 'css/eveai-chat-style.css');
}
add_action('wp_enqueue_scripts', 'eveai_chat_enqueue_scripts');
// Shortcode function
function eveai_chat_shortcode($atts) {
$options = get_option('eveai_chat_options');
$tenant_id = esc_js($options['tenant_id']);
$api_key = esc_js($options['api_key']);
$domain = esc_js($options['domain']);
$language = esc_js($options['language']);
// Generate a unique ID for this instance of the chat widget
$chat_id = 'chat-container-' . uniqid();
$output = "<div id='$chat_id'></div>";
$output .= "<script>
document.addEventListener('DOMContentLoaded', function() {
const eveAI = new EveAI(
'$tenant_id',
'$api_key',
'$domain',
'$language'
);
eveAI.initializeChat('$chat_id');
});
</script>";
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() {
?>
<div class="wrap">
<h1>EveAI Chat Settings</h1>
<form method="post" action="options.php">
<?php
settings_fields('eveai_chat_options');
do_settings_sections('eveai-chat-settings');
submit_button();
?>
</form>
</div>
<?php
}
// Register settings
function eveai_chat_register_settings() {
register_setting('eveai_chat_options', 'eveai_chat_options', 'eveai_chat_options_validate');
add_settings_section('eveai_chat_main', 'Main Settings', 'eveai_chat_section_text', 'eveai-chat-settings');
add_settings_field('eveai_chat_tenant_id', 'Tenant ID', 'eveai_chat_tenant_id_input', 'eveai-chat-settings', 'eveai_chat_main');
add_settings_field('eveai_chat_api_key', 'API Key', 'eveai_chat_api_key_input', 'eveai-chat-settings', 'eveai_chat_main');
add_settings_field('eveai_chat_domain', 'Domain', 'eveai_chat_domain_input', 'eveai-chat-settings', 'eveai_chat_main');
add_settings_field('eveai_chat_language', 'Default Language', 'eveai_chat_language_input', 'eveai-chat-settings', 'eveai_chat_main');
}
add_action('admin_init', 'eveai_chat_register_settings');
function eveai_chat_section_text() {
echo '<p>Enter your EveAI Chat configuration details below:</p>';
}
function eveai_chat_tenant_id_input() {
$options = get_option('eveai_chat_options');
echo "<input id='eveai_chat_tenant_id' name='eveai_chat_options[tenant_id]' type='text' value='" . esc_attr($options['tenant_id']) . "' />";
}
function eveai_chat_api_key_input() {
$options = get_option('eveai_chat_options');
echo "<input id='eveai_chat_api_key' name='eveai_chat_options[api_key]' type='password' value='" . esc_attr($options['api_key']) . "' />";
}
function eveai_chat_domain_input() {
$options = get_option('eveai_chat_options');
echo "<input id='eveai_chat_domain' name='eveai_chat_options[domain]' type='text' value='" . esc_attr($options['domain']) . "' />";
}
function eveai_chat_language_input() {
$options = get_option('eveai_chat_options');
echo "<input id='eveai_chat_language' name='eveai_chat_options[language]' type='text' value='" . esc_attr($options['language']) . "' />";
}
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;
}

View File

@@ -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 `
<div class="chat-container">
<div class="messages-area"></div>
<div class="question-area">
<input type="text" placeholder="Type your message here..." />
<i class="material-icons send-icon outlined">send</i>
</div>
<div class="status-line">
<i class="material-icons connection-status-icon">link_off</i>
<span class="status-message"></span>
</div>
</div>
`;
}
addUserMessage(text) {
const message = document.createElement('div');
message.classList.add('message', 'user');
message.innerHTML = `<p>${text}</p>`;
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 => `<a href="${url}" target="_blank">${url}</a>`).join('<br>');
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 = `
<p>${content}</p>
${citationsHtml ? `<p class="citations">${citationsHtml}</p>` : ''}
<div class="message-icons">
<i class="material-icons ${algorithmClass}">fingerprint</i>
<i class="material-icons thumb-icon outlined" data-feedback="up" data-interaction-id="${interactionId}">thumb_up_off_alt</i>
<i class="material-icons thumb-icon outlined" data-feedback="down" data-interaction-id="${interactionId}">thumb_down_off_alt</i>
</div>
`;
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);

View File

@@ -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 = '<eveai-chat-widget></eveai-chat-widget>';
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');
}
}
}

View File

@@ -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.