iatoolkit 0.56.0__tar.gz → 0.58.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of iatoolkit might be problematic. Click here for more details.
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/PKG-INFO +1 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/pyproject.toml +1 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/routes.py +2 -8
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/iatoolkit.py +1 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/models.py +26 -1
- iatoolkit-0.58.0/src/iatoolkit/services/auth_service.py +181 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/profile_service.py +10 -10
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/query_service.py +0 -3
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/user_session_context_service.py +1 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_main.js +2 -2
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_login_widget.html +2 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/chat.html +3 -63
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/base_login_view.py +14 -29
- iatoolkit-0.58.0/src/iatoolkit/views/external_login_view.py +83 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/init_context_api_view.py +2 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/llmquery_api_view.py +0 -6
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/login_view.py +10 -4
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/PKG-INFO +1 -1
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/SOURCES.txt +0 -1
- iatoolkit-0.56.0/src/iatoolkit/services/auth_service.py +0 -77
- iatoolkit-0.56.0/src/iatoolkit/views/external_login_view.py +0 -110
- iatoolkit-0.56.0/src/iatoolkit/views/llmquery_web_view.py +0 -38
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/readme.md +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/requirements.txt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/setup.cfg +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/base_company.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/cli_commands.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/exceptions.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/session_manager.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/util.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/company_registry.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/call_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/gemini_adapter.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_client.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_response.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/mail_app.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/openai_adapter.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/database_manager.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/document_repo.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/tasks_repo.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/benchmark_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/branding_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/document_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/excel_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/file_processor_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/history_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/jwt_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/load_documents_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/mail_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/onboarding_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/prompt_manager_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/search_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/sql_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/tasks_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_feedback.js +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_history.js +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_onboarding.js +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_info.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/format_styles.prompt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/query_main.prompt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/sql_rules.prompt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_branding_styles.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_navbar.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/about.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/base.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/change_password.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/chat_modals.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/error.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/forgot_password.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/header.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/index.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/login_simulation.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/signup.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/test.html +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/__init__.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/change_password_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/chat_token_request_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/file_store_api_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/history_api_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/index_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/login_simulation_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/prompt_api_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/signup_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/tasks_review_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/tasks_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/verify_user_view.py +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/requires.txt +0 -0
- {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/top_level.txt +0 -0
|
@@ -21,7 +21,6 @@ def register_views(injector, app):
|
|
|
21
21
|
|
|
22
22
|
from iatoolkit.views.index_view import IndexView
|
|
23
23
|
from iatoolkit.views.init_context_api_view import InitContextApiView
|
|
24
|
-
from iatoolkit.views.llmquery_web_view import LLMQueryWebView
|
|
25
24
|
from iatoolkit.views.llmquery_api_view import LLMQueryApiView
|
|
26
25
|
from iatoolkit.views.tasks_view import TaskView
|
|
27
26
|
from iatoolkit.views.tasks_review_view import TaskReviewView
|
|
@@ -67,8 +66,8 @@ def register_views(injector, app):
|
|
|
67
66
|
view_func=ChatTokenRequestView.as_view('chat-token'))
|
|
68
67
|
|
|
69
68
|
# init (reset) the company context (with api-key)
|
|
70
|
-
app.add_url_rule('/<company_short_name>/api/
|
|
71
|
-
view_func=InitContextApiView.as_view('
|
|
69
|
+
app.add_url_rule('/<company_short_name>/api/init-context',
|
|
70
|
+
view_func=InitContextApiView.as_view('init-context'))
|
|
72
71
|
|
|
73
72
|
# register new user, account verification and forgot password
|
|
74
73
|
app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
|
|
@@ -80,13 +79,8 @@ def register_views(injector, app):
|
|
|
80
79
|
|
|
81
80
|
# main chat query, used by the JS in the browser (with credentials)
|
|
82
81
|
# can be used also for executing iatoolkit prompts
|
|
83
|
-
app.add_url_rule('/<company_short_name>/llm_query', view_func=LLMQueryWebView.as_view('llm_query_web'))
|
|
84
|
-
|
|
85
|
-
# this is the same function as above, but with api-key
|
|
86
82
|
app.add_url_rule('/<company_short_name>/api/llm_query', view_func=LLMQueryApiView.as_view('llm_query_api'))
|
|
87
83
|
|
|
88
|
-
# chat buttons are here on
|
|
89
|
-
|
|
90
84
|
# open the promt directory
|
|
91
85
|
app.add_url_rule('/<company_short_name>/api/prompts', view_func=PromptApiView.as_view('prompt'))
|
|
92
86
|
|
|
@@ -19,7 +19,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|
|
19
19
|
from injector import Binder, singleton, Injector
|
|
20
20
|
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
21
21
|
|
|
22
|
-
IATOOLKIT_VERSION = "0.
|
|
22
|
+
IATOOLKIT_VERSION = "0.57.0"
|
|
23
23
|
|
|
24
24
|
# global variable for the unique instance of IAToolkit
|
|
25
25
|
_iatoolkit_instance: Optional['IAToolkit'] = None
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from sqlalchemy import Column, Integer, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
|
|
6
|
+
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
|
|
7
7
|
from sqlalchemy.orm import DeclarativeBase
|
|
8
8
|
from sqlalchemy.orm import relationship, class_mapper, declarative_base
|
|
9
|
+
from sqlalchemy.sql import func
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from pgvector.sqlalchemy import Vector
|
|
11
12
|
from enum import Enum as PyEnum
|
|
@@ -307,3 +308,27 @@ class Prompt(Base):
|
|
|
307
308
|
|
|
308
309
|
company = relationship("Company", back_populates="prompts")
|
|
309
310
|
category = relationship("PromptCategory", back_populates="prompts")
|
|
311
|
+
|
|
312
|
+
class AccessLog(Base):
|
|
313
|
+
# Modelo ORM para registrar cada intento de acceso a la plataforma.
|
|
314
|
+
__tablename__ = 'iat_access_log'
|
|
315
|
+
|
|
316
|
+
id = Column(BigInteger, primary_key=True)
|
|
317
|
+
|
|
318
|
+
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
319
|
+
company_short_name = Column(String(100), nullable=False, index=True)
|
|
320
|
+
user_identifier = Column(String(255), index=True)
|
|
321
|
+
|
|
322
|
+
# Cómo y el Resultado
|
|
323
|
+
auth_type = Column(String(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
|
|
324
|
+
outcome = Column(String(10), nullable=False) # 'success' o 'failure'
|
|
325
|
+
reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
|
|
326
|
+
|
|
327
|
+
# Contexto de la Petición
|
|
328
|
+
source_ip = Column(String(45), nullable=False)
|
|
329
|
+
user_agent_hash = Column(String(16)) # Hash corto del User-Agent
|
|
330
|
+
request_path = Column(String(255), nullable=False)
|
|
331
|
+
|
|
332
|
+
def __repr__(self):
|
|
333
|
+
return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
|
|
334
|
+
f"user='{self.user_identifier}', outcome='{self.outcome}')>")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from flask import request
|
|
7
|
+
from injector import inject
|
|
8
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
9
|
+
from iatoolkit.services.jwt_service import JWTService
|
|
10
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
11
|
+
from iatoolkit.repositories.models import AccessLog
|
|
12
|
+
from flask import request
|
|
13
|
+
import logging
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthService:
|
|
18
|
+
"""
|
|
19
|
+
Centralized service for handling authentication for all incoming requests.
|
|
20
|
+
It determines the user's identity based on either a Flask session cookie or an API Key.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@inject
|
|
24
|
+
def __init__(self, profile_service: ProfileService,
|
|
25
|
+
jwt_service: JWTService,
|
|
26
|
+
db_manager: DatabaseManager
|
|
27
|
+
):
|
|
28
|
+
self.profile_service = profile_service
|
|
29
|
+
self.jwt_service = jwt_service
|
|
30
|
+
self.db_manager = db_manager
|
|
31
|
+
|
|
32
|
+
def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
|
|
33
|
+
# try to autenticate a local user, register the event and return the result
|
|
34
|
+
auth_response = self.profile_service.login(
|
|
35
|
+
company_short_name=company_short_name,
|
|
36
|
+
email=email,
|
|
37
|
+
password=password
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if not auth_response.get('success'):
|
|
41
|
+
self.log_access(
|
|
42
|
+
company_short_name=company_short_name,
|
|
43
|
+
user_identifier=email,
|
|
44
|
+
auth_type='local',
|
|
45
|
+
outcome='failure',
|
|
46
|
+
reason_code='INVALID_CREDENTIALS',
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
self.log_access(
|
|
50
|
+
company_short_name=company_short_name,
|
|
51
|
+
auth_type='local',
|
|
52
|
+
outcome='success',
|
|
53
|
+
user_identifier=auth_response.get('user_identifier')
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return auth_response
|
|
57
|
+
|
|
58
|
+
def redeem_token_for_session(self, company_short_name: str, token: str) -> dict:
|
|
59
|
+
# redeem a token for a session, register the event and return the result
|
|
60
|
+
payload = self.jwt_service.validate_chat_jwt(token)
|
|
61
|
+
|
|
62
|
+
if not payload:
|
|
63
|
+
self.log_access(
|
|
64
|
+
company_short_name=company_short_name,
|
|
65
|
+
auth_type='redeem_token',
|
|
66
|
+
outcome='failure',
|
|
67
|
+
reason_code='JWT_INVALID'
|
|
68
|
+
)
|
|
69
|
+
return {'success': False, 'error': 'Token inválido o expirado'}
|
|
70
|
+
|
|
71
|
+
# 2. if token is valid, extract the user_identifier
|
|
72
|
+
user_identifier = payload.get('user_identifier')
|
|
73
|
+
try:
|
|
74
|
+
# create the Flask session
|
|
75
|
+
self.profile_service.set_session_for_user(company_short_name, user_identifier)
|
|
76
|
+
self.log_access(
|
|
77
|
+
company_short_name=company_short_name,
|
|
78
|
+
auth_type='redeem_token',
|
|
79
|
+
outcome='success',
|
|
80
|
+
user_identifier=user_identifier
|
|
81
|
+
)
|
|
82
|
+
return {'success': True, 'user_identifier': user_identifier}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
|
|
85
|
+
self.log_access(
|
|
86
|
+
company_short_name=company_short_name,
|
|
87
|
+
auth_type='redeem_token',
|
|
88
|
+
outcome='failure',
|
|
89
|
+
reason_code='SESSION_CREATION_FAILED',
|
|
90
|
+
user_identifier=user_identifier
|
|
91
|
+
)
|
|
92
|
+
return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
|
|
93
|
+
|
|
94
|
+
def verify(self) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
Verifies the current request and identifies the user.
|
|
97
|
+
|
|
98
|
+
Returns a dictionary with:
|
|
99
|
+
- success: bool
|
|
100
|
+
- user_identifier: str (if successful)
|
|
101
|
+
- company_short_name: str (if successful)
|
|
102
|
+
- error_message: str (on failure)
|
|
103
|
+
- status_code: int (on failure)
|
|
104
|
+
"""
|
|
105
|
+
# --- Priority 1: Check for a valid Flask web session ---
|
|
106
|
+
session_info = self.profile_service.get_current_session_info()
|
|
107
|
+
if session_info and session_info.get('user_identifier'):
|
|
108
|
+
# User is authenticated via a web session cookie.
|
|
109
|
+
return {
|
|
110
|
+
"success": True,
|
|
111
|
+
"company_short_name": session_info['company_short_name'],
|
|
112
|
+
"user_identifier": session_info['user_identifier']
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# --- Priority 2: Check for a valid API Key in headers ---
|
|
116
|
+
api_key = None
|
|
117
|
+
auth = request.headers.get('Authorization', '')
|
|
118
|
+
if isinstance(auth, str) and auth.lower().startswith('bearer '):
|
|
119
|
+
api_key = auth.split(' ', 1)[1].strip()
|
|
120
|
+
|
|
121
|
+
if api_key:
|
|
122
|
+
api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
|
|
123
|
+
if not api_key_entry:
|
|
124
|
+
logging.info(f"Invalid or inactive API Key {api_key}")
|
|
125
|
+
return {"success": False, "error": "Invalid or inactive API Key", "status_code": 401}
|
|
126
|
+
|
|
127
|
+
# obtain the company from the api_key_entry
|
|
128
|
+
company = api_key_entry.company
|
|
129
|
+
|
|
130
|
+
# For API calls, the external_user_id must be provided in the request.
|
|
131
|
+
user_identifier = ''
|
|
132
|
+
if request.is_json:
|
|
133
|
+
data = request.get_json() or {}
|
|
134
|
+
user_identifier = data.get('user_identifier', '')
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"success": True,
|
|
138
|
+
"company_short_name": company.short_name,
|
|
139
|
+
"user_identifier": user_identifier
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# --- Failure: No valid credentials found ---
|
|
143
|
+
logging.info(f"Authentication required. No session cookie or API Key provided.")
|
|
144
|
+
return {"success": False, "error": "Authentication required. No session cookie or API Key provided.",
|
|
145
|
+
"status_code": 402}
|
|
146
|
+
|
|
147
|
+
def log_access(self,
|
|
148
|
+
company_short_name: str,
|
|
149
|
+
auth_type: str,
|
|
150
|
+
outcome: str,
|
|
151
|
+
user_identifier: str = None,
|
|
152
|
+
reason_code: str = None):
|
|
153
|
+
"""
|
|
154
|
+
Registra un intento de acceso en la base de datos.
|
|
155
|
+
Es "best-effort" y no debe interrumpir el flujo de autenticación.
|
|
156
|
+
"""
|
|
157
|
+
session = self.db_manager.scoped_session()
|
|
158
|
+
try:
|
|
159
|
+
# Capturar datos del contexto de la petición de Flask
|
|
160
|
+
source_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
|
|
161
|
+
path = request.path
|
|
162
|
+
ua = request.headers.get('User-Agent', '')
|
|
163
|
+
ua_hash = hashlib.sha256(ua.encode()).hexdigest()[:16] if ua else None
|
|
164
|
+
|
|
165
|
+
# Crear la entrada de log
|
|
166
|
+
log_entry = AccessLog(
|
|
167
|
+
company_short_name=company_short_name,
|
|
168
|
+
user_identifier=user_identifier,
|
|
169
|
+
auth_type=auth_type,
|
|
170
|
+
outcome=outcome,
|
|
171
|
+
reason_code=reason_code,
|
|
172
|
+
source_ip=source_ip,
|
|
173
|
+
user_agent_hash=ua_hash,
|
|
174
|
+
request_path=path,
|
|
175
|
+
)
|
|
176
|
+
session.add(log_entry)
|
|
177
|
+
session.commit()
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logging.error(f"Fallo al escribir en AccessLog: {e}", exc_info=False)
|
|
181
|
+
session.rollback()
|
|
@@ -66,16 +66,18 @@ class ProfileService:
|
|
|
66
66
|
"extras": {}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
# 2.
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
# 2. create user_profile in context
|
|
70
|
+
self.save_user_profile(company, user_identifier, user_profile)
|
|
71
|
+
|
|
72
|
+
# 3. create the web session
|
|
73
|
+
self.set_session_for_user(company.short_name, user_identifier)
|
|
72
74
|
return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
|
|
73
75
|
except Exception as e:
|
|
74
76
|
return {'success': False, "message": str(e)}
|
|
75
77
|
|
|
76
|
-
def
|
|
78
|
+
def create_external_user_profile_context(self, company: Company, user_identifier: str):
|
|
77
79
|
"""
|
|
78
|
-
Public method for views to create a
|
|
80
|
+
Public method for views to create a user profile context for an external user.
|
|
79
81
|
"""
|
|
80
82
|
# 1. Fetch the external user profile via Dispatcher.
|
|
81
83
|
external_user_profile = self.dispatcher.get_user_info(
|
|
@@ -84,12 +86,12 @@ class ProfileService:
|
|
|
84
86
|
)
|
|
85
87
|
|
|
86
88
|
# 2. Call the session creation helper with external_user_id as user_identifier
|
|
87
|
-
self.
|
|
89
|
+
self.save_user_profile(
|
|
88
90
|
company=company,
|
|
89
91
|
user_identifier=user_identifier,
|
|
90
92
|
user_profile=external_user_profile)
|
|
91
93
|
|
|
92
|
-
def
|
|
94
|
+
def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
|
|
93
95
|
"""
|
|
94
96
|
Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
|
|
95
97
|
"""
|
|
@@ -102,10 +104,8 @@ class ProfileService:
|
|
|
102
104
|
# save user_profile in Redis session
|
|
103
105
|
self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
|
|
104
106
|
|
|
105
|
-
# save a min Flask session cookie for this user
|
|
106
|
-
self.set_session_for_user(company.short_name, user_identifier)
|
|
107
|
-
|
|
108
107
|
def set_session_for_user(self, company_short_name: str, user_identifier:str ):
|
|
108
|
+
# save a min Flask session cookie for this user
|
|
109
109
|
SessionManager.set('company_short_name', company_short_name)
|
|
110
110
|
SessionManager.set('user_identifier', user_identifier)
|
|
111
111
|
|
|
@@ -66,9 +66,6 @@ class QueryService:
|
|
|
66
66
|
|
|
67
67
|
# Get the user profile from the single source of truth.
|
|
68
68
|
user_profile = self.profile_service.get_profile_by_identifier(company_short_name, user_identifier)
|
|
69
|
-
if not user_profile:
|
|
70
|
-
# This might happen if a session exists for a user that was deleted.
|
|
71
|
-
return None, None
|
|
72
69
|
|
|
73
70
|
# render the iatoolkit main system prompt with the company/user information
|
|
74
71
|
system_prompt_template = self.prompt_service.get_system_prompt()
|
{iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/user_session_context_service.py
RENAMED
|
@@ -23,7 +23,7 @@ class UserSessionContextService:
|
|
|
23
23
|
return f"session:{company_short_name}/{user_identifier}"
|
|
24
24
|
|
|
25
25
|
def clear_all_context(self, company_short_name: str, user_identifier: str):
|
|
26
|
-
"""Limpia el contexto
|
|
26
|
+
"""Limpia el contexto del LLM en la sesión para un usuario de forma atómica."""
|
|
27
27
|
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
28
28
|
if session_key:
|
|
29
29
|
# RedisSessionManager.remove(session_key)
|
|
@@ -6,7 +6,7 @@ let selectedPrompt = null; // Will hold a lightweight prompt object
|
|
|
6
6
|
|
|
7
7
|
$(document).ready(function () {
|
|
8
8
|
// Gatilla el redeem sin esperar ni manejar respuesta aquí
|
|
9
|
-
if (window.redeemToken
|
|
9
|
+
if (window.redeemToken) {
|
|
10
10
|
const url = `/api/redeem_token`;
|
|
11
11
|
// No await: dejamos que callToolkit maneje todo internamente
|
|
12
12
|
callToolkit(url, {'token': window.redeemToken}, "POST").catch(() => {});
|
|
@@ -183,7 +183,7 @@ const handleChatMessage = async function () {
|
|
|
183
183
|
user_identifier: window.user_identifier
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
const responseData = await callToolkit("/llm_query", data, "POST");
|
|
186
|
+
const responseData = await callToolkit("/api/llm_query", data, "POST");
|
|
187
187
|
if (responseData && responseData.answer) {
|
|
188
188
|
const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
|
|
189
189
|
displayBotMessage(answerSection);
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
method="post">
|
|
15
15
|
<div class="mb-3">
|
|
16
16
|
<label for="email" class="form-label d-block">Correo Electrónico</label>
|
|
17
|
-
<input type="email" id="email" name="email" class="form-control"
|
|
17
|
+
<input type="email" id="email" name="email" class="form-control"
|
|
18
|
+
required value="{{ form_data.email or '' }}">
|
|
18
19
|
</div>
|
|
19
20
|
<div class="mb-3">
|
|
20
21
|
<label for="password" class="form-label d-block">Contraseña</label>
|
|
@@ -183,11 +183,12 @@
|
|
|
183
183
|
// --- Global Configuration from Backend ---
|
|
184
184
|
window.companyShortName = "{{ company_short_name }}";
|
|
185
185
|
window.user_identifier = "{{ user_identifier }}";
|
|
186
|
-
window.redeemToken =
|
|
186
|
+
window.redeemToken = {{ redeem_token | tojson | default('null') }};
|
|
187
187
|
window.iatoolkit_base_url = "{{ iatoolkit_base_url }}";
|
|
188
188
|
window.availablePrompts = {{ prompts.message | tojson }};
|
|
189
189
|
window.onboardingCards = {{ onboarding_cards | tojson }};
|
|
190
190
|
window.sendButtonColor = "{{ branding.send_button_color }}";
|
|
191
|
+
|
|
191
192
|
</script>
|
|
192
193
|
|
|
193
194
|
<!-- Carga de los scripts JS externos después de definir las variables globales -->
|
|
@@ -195,7 +196,7 @@
|
|
|
195
196
|
<script src="{{ url_for('static', filename='js/chat_filepond.js', _external=True) }}"></script>
|
|
196
197
|
<script src="{{ url_for('static', filename='js/chat_history.js', _external=True) }}"></script>
|
|
197
198
|
<script src="{{ url_for('static', filename='js/chat_feedback.js', _external=True) }}"></script>
|
|
198
|
-
<script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
|
|
199
|
+
<script src="{{ url_for('static', filename='js/chat_context_reload.js', _external=True) }}"></script><script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
|
|
199
200
|
|
|
200
201
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
|
|
201
202
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
@@ -223,67 +224,6 @@
|
|
|
223
224
|
})
|
|
224
225
|
});
|
|
225
226
|
|
|
226
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
227
|
-
const reloadButton = document.getElementById('force-reload-button');
|
|
228
|
-
if (!reloadButton) return;
|
|
229
|
-
|
|
230
|
-
const originalIconClass = 'bi bi-arrow-clockwise';
|
|
231
|
-
const spinnerIconClass = 'spinner-border spinner-border-sm';
|
|
232
|
-
|
|
233
|
-
// Configuración de Toastr para que aparezca abajo a la derecha
|
|
234
|
-
toastr.options = { "positionClass": "toast-bottom-right", "preventDuplicates": true };
|
|
235
|
-
|
|
236
|
-
reloadButton.addEventListener('click', function(event) {
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
|
|
239
|
-
if (reloadButton.disabled) return; // Prevenir doble clic
|
|
240
|
-
|
|
241
|
-
// 1. Deshabilitar y mostrar spinner
|
|
242
|
-
reloadButton.disabled = true;
|
|
243
|
-
const icon = reloadButton.querySelector('i');
|
|
244
|
-
icon.className = spinnerIconClass;
|
|
245
|
-
toastr.info('Iniciando recarga de contexto en segundo plano...');
|
|
246
|
-
|
|
247
|
-
// 2. Construir la URL dinámicamente
|
|
248
|
-
const company = window.companyShortName;
|
|
249
|
-
const reloadUrl = `/${company}/api/init_context_api`;
|
|
250
|
-
|
|
251
|
-
// 3. Hacer la llamada AJAX con POST
|
|
252
|
-
fetch(reloadUrl, {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
headers: {
|
|
255
|
-
'Content-Type': 'application/json'
|
|
256
|
-
},
|
|
257
|
-
// Envía un cuerpo vacío o los datos necesarios
|
|
258
|
-
body: JSON.stringify({})
|
|
259
|
-
})
|
|
260
|
-
.then(response => {
|
|
261
|
-
if (!response.ok) {
|
|
262
|
-
return response.json().then(err => {
|
|
263
|
-
throw new Error(err.error_message || `Error del servidor: ${response.status}`);
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return response.json();
|
|
267
|
-
})
|
|
268
|
-
.then(data => {
|
|
269
|
-
if (data.status === 'OK') {
|
|
270
|
-
toastr.success(data.message || 'Contexto recargado exitosamente.');
|
|
271
|
-
} else {
|
|
272
|
-
toastr.error(data.error_message || 'Ocurrió un error desconocido.');
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
.catch(error => {
|
|
276
|
-
console.error('Error durante la recarga del contexto:', error);
|
|
277
|
-
toastr.error(error.message || 'Error de red al intentar recargar.');
|
|
278
|
-
})
|
|
279
|
-
.finally(() => {
|
|
280
|
-
// 4. Restaurar el botón
|
|
281
|
-
reloadButton.disabled = false;
|
|
282
|
-
icon.className = originalIconClass;
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
227
|
// Inicialización del modal de onboarding
|
|
288
228
|
document.addEventListener('DOMContentLoaded', function () {
|
|
289
229
|
const btn = document.getElementById('onboarding-button');
|
|
@@ -8,11 +8,13 @@ from flask.views import MethodView
|
|
|
8
8
|
from flask import render_template, url_for
|
|
9
9
|
from injector import inject
|
|
10
10
|
from iatoolkit.services.profile_service import ProfileService
|
|
11
|
+
from iatoolkit.services.auth_service import AuthService
|
|
11
12
|
from iatoolkit.services.query_service import QueryService
|
|
12
13
|
from iatoolkit.services.branding_service import BrandingService
|
|
13
14
|
from iatoolkit.services.onboarding_service import OnboardingService
|
|
14
15
|
from iatoolkit.services.prompt_manager_service import PromptService
|
|
15
16
|
from iatoolkit.services.jwt_service import JWTService
|
|
17
|
+
from iatoolkit.repositories.models import Company
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class BaseLoginView(MethodView):
|
|
@@ -23,6 +25,7 @@ class BaseLoginView(MethodView):
|
|
|
23
25
|
@inject
|
|
24
26
|
def __init__(self,
|
|
25
27
|
profile_service: ProfileService,
|
|
28
|
+
auth_service: AuthService,
|
|
26
29
|
jwt_service: JWTService,
|
|
27
30
|
branding_service: BrandingService,
|
|
28
31
|
prompt_service: PromptService,
|
|
@@ -30,6 +33,7 @@ class BaseLoginView(MethodView):
|
|
|
30
33
|
query_service: QueryService
|
|
31
34
|
):
|
|
32
35
|
self.profile_service = profile_service
|
|
36
|
+
self.auth_service = auth_service
|
|
33
37
|
self.jwt_service = jwt_service
|
|
34
38
|
self.branding_service = branding_service
|
|
35
39
|
self.prompt_service = prompt_service
|
|
@@ -37,44 +41,26 @@ class BaseLoginView(MethodView):
|
|
|
37
41
|
self.query_service = query_service
|
|
38
42
|
|
|
39
43
|
|
|
40
|
-
def _handle_login_path(self,
|
|
44
|
+
def _handle_login_path(self,
|
|
45
|
+
company: Company,
|
|
46
|
+
user_identifier: str,
|
|
47
|
+
target_url: str,
|
|
48
|
+
redeem_token: str = None):
|
|
41
49
|
"""
|
|
42
50
|
Centralized logic to decide between the fast path and the slow path.
|
|
43
51
|
"""
|
|
44
|
-
# --- Get the company branding
|
|
52
|
+
# --- Get the company branding and onboarding_cards
|
|
45
53
|
branding_data = self.branding_service.get_company_branding(company)
|
|
54
|
+
onboarding_cards = self.onboarding_service.get_onboarding_cards(company)
|
|
55
|
+
company_short_name = company.short_name
|
|
46
56
|
|
|
47
57
|
# this service decides is the context needs to be rebuilt or not
|
|
48
58
|
prep_result = self.query_service.prepare_context(
|
|
49
|
-
company_short_name=
|
|
59
|
+
company_short_name=company.short_name, user_identifier=user_identifier
|
|
50
60
|
)
|
|
51
61
|
|
|
52
|
-
# generate continuation token for external login
|
|
53
|
-
redeem_token = ''
|
|
54
|
-
if self.__class__.__name__ == 'ExternalLoginView':
|
|
55
|
-
redeem_token = self.jwt_service.generate_chat_jwt(
|
|
56
|
-
company_short_name=company_short_name,
|
|
57
|
-
user_identifier=user_identifier,
|
|
58
|
-
expires_delta_seconds=300
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if not redeem_token:
|
|
62
|
-
return "Error al generar el redeem_token para login externo.", 500
|
|
63
|
-
|
|
64
62
|
if prep_result.get('rebuild_needed'):
|
|
65
63
|
# --- SLOW PATH: Render the loading shell ---
|
|
66
|
-
onboarding_cards = self.onboarding_service.get_onboarding_cards(company)
|
|
67
|
-
|
|
68
|
-
# callback url to call when the context finish loading
|
|
69
|
-
if redeem_token:
|
|
70
|
-
target_url = url_for('finalize_with_token',
|
|
71
|
-
company_short_name=company_short_name,
|
|
72
|
-
token=redeem_token,
|
|
73
|
-
_external=True)
|
|
74
|
-
else:
|
|
75
|
-
target_url = url_for('finalize_no_token',
|
|
76
|
-
company_short_name=company_short_name,
|
|
77
|
-
_external=True)
|
|
78
64
|
return render_template(
|
|
79
65
|
"onboarding_shell.html",
|
|
80
66
|
iframe_src_url=target_url,
|
|
@@ -84,13 +70,12 @@ class BaseLoginView(MethodView):
|
|
|
84
70
|
else:
|
|
85
71
|
# --- FAST PATH: Render the chat page directly ---
|
|
86
72
|
prompts = self.prompt_service.get_user_prompts(company_short_name)
|
|
87
|
-
onboarding_cards = self.onboarding_service.get_onboarding_cards(company)
|
|
88
73
|
return render_template(
|
|
89
74
|
"chat.html",
|
|
90
75
|
company_short_name=company_short_name,
|
|
91
76
|
user_identifier=user_identifier,
|
|
92
|
-
branding=branding_data,
|
|
93
77
|
prompts=prompts,
|
|
78
|
+
branding=branding_data,
|
|
94
79
|
onboarding_cards=onboarding_cards,
|
|
95
80
|
redeem_token=redeem_token
|
|
96
81
|
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from flask import request, jsonify, url_for
|
|
9
|
+
from injector import inject
|
|
10
|
+
from iatoolkit.views.base_login_view import BaseLoginView
|
|
11
|
+
|
|
12
|
+
# Importar los servicios que necesita la clase base
|
|
13
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
14
|
+
from iatoolkit.services.jwt_service import JWTService
|
|
15
|
+
|
|
16
|
+
class ExternalLoginView(BaseLoginView):
|
|
17
|
+
"""
|
|
18
|
+
Handles login for external users via API.
|
|
19
|
+
Authenticates and then delegates the path decision (fast/slow) to the base class.
|
|
20
|
+
"""
|
|
21
|
+
def post(self, company_short_name: str):
|
|
22
|
+
data = request.get_json()
|
|
23
|
+
if not data or 'user_identifier' not in data:
|
|
24
|
+
return jsonify({"error": "Falta user_identifier"}), 400
|
|
25
|
+
|
|
26
|
+
company = self.profile_service.get_company_by_short_name(company_short_name)
|
|
27
|
+
if not company:
|
|
28
|
+
return jsonify({"error": "Empresa no encontrada"}), 404
|
|
29
|
+
|
|
30
|
+
user_identifier = data.get('user_identifier')
|
|
31
|
+
if not user_identifier:
|
|
32
|
+
return jsonify({"error": "missing user_identifier"}), 404
|
|
33
|
+
|
|
34
|
+
# 1. Authenticate the API call.
|
|
35
|
+
auth_response = self.auth_service.verify()
|
|
36
|
+
if not auth_response.get("success"):
|
|
37
|
+
return jsonify(auth_response), 401
|
|
38
|
+
|
|
39
|
+
# 2. Create the external user session.
|
|
40
|
+
self.profile_service.create_external_user_profile_context(company, user_identifier)
|
|
41
|
+
|
|
42
|
+
# 3. create a redeem_token for create session at the end of the process
|
|
43
|
+
redeem_token = self.jwt_service.generate_chat_jwt(
|
|
44
|
+
company_short_name=company_short_name,
|
|
45
|
+
user_identifier=user_identifier,
|
|
46
|
+
expires_delta_seconds=300
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not redeem_token:
|
|
50
|
+
return jsonify({"error": "Error al generar el redeem_token para login externo."}), 403
|
|
51
|
+
|
|
52
|
+
# 4. define URL to call when slow path is finished
|
|
53
|
+
target_url = url_for('finalize_with_token',
|
|
54
|
+
company_short_name=company_short_name,
|
|
55
|
+
token=redeem_token,
|
|
56
|
+
_external=True)
|
|
57
|
+
|
|
58
|
+
# 5. Delegate the path decision to the centralized logic.
|
|
59
|
+
try:
|
|
60
|
+
return self._handle_login_path(company, user_identifier, target_url, redeem_token)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
|
|
63
|
+
return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RedeemTokenApiView(BaseLoginView):
|
|
67
|
+
# this endpoint is only used ONLY by chat_main.js to redeem a chat token
|
|
68
|
+
def post(self, company_short_name: str):
|
|
69
|
+
data = request.get_json()
|
|
70
|
+
if not data or 'token' not in data:
|
|
71
|
+
return jsonify({"error": "Falta token de validación"}), 400
|
|
72
|
+
|
|
73
|
+
# get the token and validate with auth service
|
|
74
|
+
token = data.get('token')
|
|
75
|
+
redeem_result = self.auth_service.redeem_token_for_session(
|
|
76
|
+
company_short_name=company_short_name,
|
|
77
|
+
token=token
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not redeem_result['success']:
|
|
81
|
+
return {"error": redeem_result['error']}, 401
|
|
82
|
+
|
|
83
|
+
return {"status": "ok"}, 200
|