iatoolkit 0.55.3__tar.gz → 0.57.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.

Files changed (120) hide show
  1. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/PKG-INFO +1 -1
  2. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/pyproject.toml +1 -1
  3. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/base_company.py +1 -1
  4. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/common/routes.py +23 -11
  5. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/iatoolkit.py +3 -4
  6. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/models.py +26 -1
  7. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/profile_repo.py +7 -3
  8. iatoolkit-0.57.0/src/iatoolkit/services/auth_service.py +181 -0
  9. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/dispatcher_service.py +2 -24
  10. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/jwt_service.py +15 -24
  11. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/profile_service.py +10 -8
  12. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_feedback.js +1 -1
  13. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_history.js +1 -1
  14. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_main.js +40 -14
  15. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/chat.html +7 -3
  16. iatoolkit-0.57.0/src/iatoolkit/templates/login_simulation.html +34 -0
  17. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/base_login_view.py +35 -4
  18. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/external_login_view.py +25 -28
  19. iatoolkit-0.57.0/src/iatoolkit/views/login_simulation_view.py +81 -0
  20. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/login_view.py +30 -13
  21. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/PKG-INFO +1 -1
  22. iatoolkit-0.55.3/src/iatoolkit/services/auth_service.py +0 -74
  23. iatoolkit-0.55.3/src/iatoolkit/templates/login_simulation.html +0 -91
  24. iatoolkit-0.55.3/src/iatoolkit/views/login_simulation_view.py +0 -27
  25. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/readme.md +0 -0
  26. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/requirements.txt +0 -0
  27. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/setup.cfg +0 -0
  28. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/__init__.py +0 -0
  29. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/cli_commands.py +0 -0
  30. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/common/__init__.py +0 -0
  31. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/common/exceptions.py +0 -0
  32. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/common/session_manager.py +0 -0
  33. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/common/util.py +0 -0
  34. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/company_registry.py +0 -0
  35. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/__init__.py +0 -0
  36. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/call_service.py +0 -0
  37. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  38. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  39. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  40. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  41. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  42. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
  43. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  44. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/gemini_adapter.py +0 -0
  45. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
  46. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_client.py +0 -0
  47. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
  48. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_response.py +0 -0
  49. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/mail_app.py +0 -0
  50. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/openai_adapter.py +0 -0
  51. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  52. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/__init__.py +0 -0
  53. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/database_manager.py +0 -0
  54. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/document_repo.py +0 -0
  55. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
  56. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/tasks_repo.py +0 -0
  57. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
  58. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/__init__.py +0 -0
  59. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/benchmark_service.py +0 -0
  60. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/branding_service.py +0 -0
  61. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/document_service.py +0 -0
  62. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/excel_service.py +0 -0
  63. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/file_processor_service.py +0 -0
  64. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/history_service.py +0 -0
  65. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/load_documents_service.py +0 -0
  66. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/mail_service.py +0 -0
  67. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/onboarding_service.py +0 -0
  68. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/prompt_manager_service.py +0 -0
  69. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/query_service.py +0 -0
  70. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/search_service.py +0 -0
  71. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/sql_service.py +0 -0
  72. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/tasks_service.py +0 -0
  73. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
  74. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
  75. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  76. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  77. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_onboarding.js +0 -0
  78. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  79. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_info.css +0 -0
  80. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  81. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
  82. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
  83. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
  84. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/format_styles.prompt +0 -0
  85. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/query_main.prompt +0 -0
  86. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/sql_rules.prompt +0 -0
  87. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/_branding_styles.html +0 -0
  88. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/_login_widget.html +0 -0
  89. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/_navbar.html +0 -0
  90. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/about.html +0 -0
  91. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/base.html +0 -0
  92. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/change_password.html +0 -0
  93. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/chat_modals.html +0 -0
  94. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/error.html +0 -0
  95. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/forgot_password.html +0 -0
  96. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/header.html +0 -0
  97. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/index.html +0 -0
  98. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
  99. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/signup.html +0 -0
  100. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/templates/test.html +0 -0
  101. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/__init__.py +0 -0
  102. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/change_password_view.py +0 -0
  103. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/chat_token_request_view.py +0 -0
  104. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/file_store_api_view.py +0 -0
  105. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
  106. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/history_api_view.py +0 -0
  107. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/index_view.py +0 -0
  108. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
  109. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
  110. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/llmquery_web_view.py +0 -0
  111. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/prompt_api_view.py +0 -0
  112. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/signup_view.py +0 -0
  113. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/tasks_review_view.py +0 -0
  114. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/tasks_view.py +0 -0
  115. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  116. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit/views/verify_user_view.py +0 -0
  117. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/SOURCES.txt +0 -0
  118. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  119. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/requires.txt +0 -0
  120. {iatoolkit-0.55.3 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 0.55.3
3
+ Version: 0.57.0
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iatoolkit"
7
- version = "0.55.3"
7
+ version = "0.57.0"
8
8
  requires-python = ">=3.12"
9
9
  description = "IAToolkit"
10
10
  readme = "readme.md"
@@ -88,7 +88,7 @@ class BaseCompany(ABC):
88
88
 
89
89
  @abstractmethod
90
90
  # get context specific for this company
91
- def get_user_info(self, user_identifier: str) -> str:
91
+ def get_user_info(self, user_identifier: str) -> dict:
92
92
  raise NotImplementedError("La subclase debe implementar el método get_user_info()")
93
93
 
94
94
  @abstractmethod
@@ -26,8 +26,6 @@ def register_views(injector, app):
26
26
  from iatoolkit.views.tasks_view import TaskView
27
27
  from iatoolkit.views.tasks_review_view import TaskReviewView
28
28
  from iatoolkit.views.login_simulation_view import LoginSimulationView
29
- from iatoolkit.views.login_view import LoginView, FinalizeContextView
30
- from iatoolkit.views.external_login_view import ExternalLoginView
31
29
  from iatoolkit.views.signup_view import SignupView
32
30
  from iatoolkit.views.verify_user_view import VerifyAccountView
33
31
  from iatoolkit.views.forgot_password_view import ForgotPasswordView
@@ -36,27 +34,41 @@ def register_views(injector, app):
36
34
  from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
37
35
  from iatoolkit.views.prompt_api_view import PromptApiView
38
36
  from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
37
+ from iatoolkit.views.login_view import LoginView, FinalizeContextView
38
+ from iatoolkit.views.external_login_view import ExternalLoginView, RedeemTokenApiView
39
39
 
40
40
  # iatoolkit home page
41
41
  app.add_url_rule('/<company_short_name>', view_func=IndexView.as_view('index'))
42
42
 
43
- # init (reset) the company context (with api-key)
44
- app.add_url_rule('/<company_short_name>/api/init_context_api',
45
- view_func=InitContextApiView.as_view('init_context_api'))
43
+ # login for the iatoolkit integrated frontend
44
+ app.add_url_rule('/<company_short_name>/login', view_func=LoginView.as_view('login'))
46
45
 
47
- # this functions are for login external users (with api-key)
48
- # only the first one should be used from an external app
46
+ # this is the login for external users
49
47
  app.add_url_rule('/<company_short_name>/external_login',
50
48
  view_func=ExternalLoginView.as_view('external_login'))
51
49
 
50
+ # this endpoint is called when onboarding_shell finish the context load
51
+ app.add_url_rule(
52
+ '/<company_short_name>/finalize',
53
+ view_func=FinalizeContextView.as_view('finalize_no_token')
54
+ )
55
+
56
+ app.add_url_rule(
57
+ '/<company_short_name>/finalize/<token>',
58
+ view_func=FinalizeContextView.as_view('finalize_with_token')
59
+ )
60
+
61
+ # this endpoint is called by the JS for changing the token for a session
62
+ app.add_url_rule('/<string:company_short_name>/api/redeem_token',
63
+ view_func = RedeemTokenApiView.as_view('redeem_token'))
64
+
52
65
  # this endpoint is for requesting a chat token for external users
53
66
  app.add_url_rule('/auth/chat_token',
54
67
  view_func=ChatTokenRequestView.as_view('chat-token'))
55
68
 
56
- # login for the iatoolkit integrated frontend
57
- # this is the main login endpoint for the frontend
58
- app.add_url_rule('/<company_short_name>/login', view_func=LoginView.as_view('login'))
59
- app.add_url_rule('/<company_short_name>/finalize_context_load', view_func=FinalizeContextView.as_view('finalize_context_load'))
69
+ # init (reset) the company context (with api-key)
70
+ app.add_url_rule('/<company_short_name>/api/init_context_api',
71
+ view_func=InitContextApiView.as_view('init_context_api'))
60
72
 
61
73
  # register new user, account verification and forgot password
62
74
  app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
@@ -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.55.3"
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
@@ -155,10 +155,9 @@ class IAToolkit:
155
155
 
156
156
  self.app.config.update({
157
157
  'VERSION': self.version,
158
- 'SERVER_NAME': domain,
159
158
  'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
160
- 'SESSION_COOKIE_SAMESITE': "None" if is_https else "Lax",
161
- 'SESSION_COOKIE_SECURE': is_https,
159
+ 'SESSION_COOKIE_SAMESITE': "None",
160
+ 'SESSION_COOKIE_SECURE': True,
162
161
  'SESSION_PERMANENT': False,
163
162
  'SESSION_USE_SIGNER': True,
164
163
  'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
@@ -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}')>")
@@ -72,10 +72,14 @@ class ProfileRepo:
72
72
  def create_company(self, new_company: Company):
73
73
  company = self.session.query(Company).filter_by(name=new_company.name).first()
74
74
  if company:
75
- company.parameters = new_company.parameters
76
- company.branding = new_company.branding
77
- company.onboarding_cards = new_company.onboarding_cards
75
+ if company.parameters != new_company.parameters:
76
+ company.parameters = new_company.parameters
77
+ if company.branding != new_company.branding:
78
+ company.branding = new_company.branding
79
+ if company.onboarding_cards != new_company.onboarding_cards:
80
+ company.onboarding_cards = new_company.onboarding_cards
78
81
  else:
82
+ # Si la compañía no existe, la añade a la sesión.
79
83
  self.session.add(new_company)
80
84
  company = new_company
81
85
 
@@ -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()
@@ -178,35 +178,13 @@ class Dispatcher:
178
178
  # source 2: external company user
179
179
  company_instance = self.company_instances[company_name]
180
180
  try:
181
- raw_user_data = company_instance.get_user_info(user_identifier)
181
+ external_user_profile = company_instance.get_user_info(user_identifier)
182
182
  except Exception as e:
183
183
  logging.exception(e)
184
184
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
185
185
  f"Error en get_user_info de {company_name}: {str(e)}") from e
186
186
 
187
- # always normalize the data for consistent structure
188
- return self._normalize_user_data(raw_user_data)
189
-
190
- def _normalize_user_data(self, raw_data: dict) -> dict:
191
- """
192
- Asegura que los datos del usuario siempre tengan una estructura consistente.
193
- """
194
- # default values
195
- normalized_user = {
196
- "id": raw_data.get("id", 0),
197
- "username": raw_data.get("id", 0),
198
- "user_email": raw_data.get("email", ""),
199
- "user_fullname": raw_data.get("user_fullname", ""),
200
- "is_local": False,
201
- "extras": raw_data.get("extras", {})
202
- }
203
-
204
- # get the extras from the raw data, if any
205
- extras = raw_data.get("extras", {})
206
- if isinstance(extras, dict):
207
- normalized_user.update(extras)
208
-
209
- return normalized_user
187
+ return external_user_profile
210
188
 
211
189
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
212
190
  if company_name not in self.company_instances:
@@ -24,16 +24,18 @@ class JWTService:
24
24
  raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
25
25
 
26
26
  def generate_chat_jwt(self,
27
- company_id: int,
28
27
  company_short_name: str,
29
- external_user_id: str,
28
+ user_identifier: str,
30
29
  expires_delta_seconds: int) -> Optional[str]:
31
30
  # generate a JWT for a chat session
32
31
  try:
32
+ if not company_short_name or not user_identifier:
33
+ logging.error(f"Missing token ID: {company_short_name}/{user_identifier}")
34
+ return None
35
+
33
36
  payload = {
34
- 'company_id': company_id,
35
37
  'company_short_name': company_short_name,
36
- 'external_user_id': external_user_id,
38
+ 'user_identifier': user_identifier,
37
39
  'exp': time.time() + expires_delta_seconds,
38
40
  'iat': time.time(),
39
41
  'type': 'chat_session' # Identificador del tipo de token
@@ -41,10 +43,10 @@ class JWTService:
41
43
  token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
42
44
  return token
43
45
  except Exception as e:
44
- logging.error(f"Error al generar JWT para company {company_id}, user {external_user_id}: {e}")
46
+ logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
45
47
  return None
46
48
 
47
- def validate_chat_jwt(self, token: str, expected_company_short_name: str) -> Optional[Dict[str, Any]]:
49
+ def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
48
50
  """
49
51
  Valida un JWT de sesión de chat.
50
52
  Retorna el payload decodificado si es válido y coincide con la empresa, o None.
@@ -59,33 +61,22 @@ class JWTService:
59
61
  logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
60
62
  return None
61
63
 
62
- if payload.get('company_short_name') != expected_company_short_name:
63
- logging.warning(
64
- f"Validación JWT fallida: company_short_name no coincide. "
65
- f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
66
- )
64
+ # user_identifier debe estar presente
65
+ if not payload.get('user_identifier'):
66
+ logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
67
67
  return None
68
68
 
69
- # external_user_id debe estar presente
70
- if 'external_user_id' not in payload or not payload['external_user_id']:
71
- logging.warning(f"Validación JWT fallida: external_user_id ausente o vacío.")
72
- return None
73
-
74
- # company_id debe estar presente
75
- if 'company_id' not in payload or not isinstance(payload['company_id'], int):
76
- logging.warning(f"Validación JWT fallida: company_id ausente o tipo incorrecto.")
69
+ if not payload.get('company_short_name'):
70
+ logging.warning(f"Validación JWT fallida: company_short_name ausente.")
77
71
  return None
78
72
 
79
73
  logging.debug(
80
74
  f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
81
75
  return payload
82
76
 
83
- except jwt.ExpiredSignatureError:
84
- logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
85
- return None
86
77
  except jwt.InvalidTokenError as e:
87
- logging.warning(f"Validación JWT fallida: token inválido para {expected_company_short_name}. Error: {e}")
78
+ logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
88
79
  return None
89
80
  except Exception as e:
90
- logging.error(f"Error inesperado durante validación de JWT para {expected_company_short_name}: {e}")
81
+ logging.error(f"Error inesperado durante validación de JWT : {e}")
91
82
  return None
@@ -60,7 +60,6 @@ class ProfileService:
60
60
  # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
61
61
  user_identifier = user.email # no longer de ID
62
62
  user_profile = {
63
- "id": user_identifier,
64
63
  "user_email": user.email,
65
64
  "user_fullname": f'{user.first_name} {user.last_name}',
66
65
  "user_is_local": True,
@@ -78,8 +77,8 @@ class ProfileService:
78
77
  """
79
78
  Public method for views to create a web session for an external user.
80
79
  """
81
- # 1. Fetch the profile from the external system via Dispatcher.
82
- user_profile = self.dispatcher.get_user_info(
80
+ # 1. Fetch the external user profile via Dispatcher.
81
+ external_user_profile = self.dispatcher.get_user_info(
83
82
  company_name=company.short_name,
84
83
  user_identifier=user_identifier
85
84
  )
@@ -88,7 +87,7 @@ class ProfileService:
88
87
  self.create_web_session(
89
88
  company=company,
90
89
  user_identifier=user_identifier,
91
- user_profile=user_profile)
90
+ user_profile=external_user_profile)
92
91
 
93
92
  def create_web_session(self, company: Company, user_identifier: str, user_profile: dict):
94
93
  """
@@ -96,16 +95,19 @@ class ProfileService:
96
95
  """
97
96
  user_profile['company_short_name'] = company.short_name
98
97
  user_profile['user_identifier'] = user_identifier
99
- user_profile['user_id'] = user_identifier
98
+ user_profile['id'] = user_identifier
100
99
  user_profile['company_id'] = company.id
101
100
  user_profile['company'] = company.name
102
101
 
103
- # user_profile['last_activity'] = datetime.now(timezone.utc).timestamp()
102
+ # save user_profile in Redis session
104
103
  self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
105
104
 
106
- # save this values into Flask session cookie
105
+ # save a min Flask session cookie for this user
106
+ self.set_session_for_user(company.short_name, user_identifier)
107
+
108
+ def set_session_for_user(self, company_short_name: str, user_identifier:str ):
109
+ SessionManager.set('company_short_name', company_short_name)
107
110
  SessionManager.set('user_identifier', user_identifier)
108
- SessionManager.set('company_short_name', company.short_name)
109
111
 
110
112
  def get_current_session_info(self) -> dict:
111
113
  """
@@ -106,7 +106,7 @@ const sendFeedback = async function(message) {
106
106
  };
107
107
  try {
108
108
  // Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
109
- const responseData = await callLLMAPI('/feedback', data, "POST");
109
+ const responseData = await callToolkit('/feedback', data, "POST");
110
110
  return responseData;
111
111
  } catch (error) {
112
112
  console.error("Error al enviar feedback:", error);
@@ -40,7 +40,7 @@ $(document).ready(function () {
40
40
  historyContent.hide();
41
41
 
42
42
  try {
43
- const responseData = await callLLMAPI("/api/history", {}, "POST");
43
+ const responseData = await callToolkit("/api/history", {}, "POST");
44
44
 
45
45
  if (responseData && responseData.history) {
46
46
  // Guardar datos globalmente
@@ -5,6 +5,13 @@ let abortController = null;
5
5
  let selectedPrompt = null; // Will hold a lightweight prompt object
6
6
 
7
7
  $(document).ready(function () {
8
+ // Gatilla el redeem sin esperar ni manejar respuesta aquí
9
+ if (window.redeemToken !== '') {
10
+ const url = `/api/redeem_token`;
11
+ // No await: dejamos que callToolkit maneje todo internamente
12
+ callToolkit(url, {'token': window.redeemToken}, "POST").catch(() => {});
13
+ }
14
+
8
15
  // --- MAIN EVENT HANDLERS ---
9
16
  $('#send-button').on('click', handleChatMessage);
10
17
  $('#stop-button').on('click', abortCurrentRequest);
@@ -176,7 +183,7 @@ const handleChatMessage = async function () {
176
183
  user_identifier: window.user_identifier
177
184
  };
178
185
 
179
- const responseData = await callLLMAPI("/llm_query", data, "POST");
186
+ const responseData = await callToolkit("/llm_query", data, "POST");
180
187
  if (responseData && responseData.answer) {
181
188
  const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
182
189
  displayBotMessage(answerSection);
@@ -292,28 +299,39 @@ function resetSpecificDataInput() {
292
299
  * @param {number} timeoutMs - Timeout in milliseconds.
293
300
  * @returns {Promise<object|null>} The response data or null on error.
294
301
  */
295
- const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
302
+ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
296
303
  const url = `${window.iatoolkit_base_url}/${window.companyShortName}${apiPath}`;
297
304
 
298
- const headers = {"Content-Type": "application/json"};
299
- if (window.sessionJWT) {
300
- headers['X-Chat-Token'] = window.sessionJWT;
301
- }
302
-
303
305
  abortController = new AbortController();
304
306
  const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
305
307
 
306
308
  try {
307
- const response = await fetch(url, {
308
- method: method,
309
- headers: headers,
310
- body: JSON.stringify(data),
311
- signal: abortController.signal, // Se usa el signal del controlador global
312
- credentials: 'include'
313
- });
309
+ const fetchOptions = {
310
+ method: method,
311
+ signal: abortController.signal,
312
+ credentials: 'include'
313
+ };
314
+
315
+ // Solo agrega body si el método lo soporta y hay datos
316
+ const methodUpper = (method || '').toUpperCase();
317
+ const canHaveBody = !['GET', 'HEAD'].includes(methodUpper);
318
+ if (canHaveBody && data !== undefined && data !== null) {
319
+ fetchOptions.body = JSON.stringify(data);
320
+ fetchOptions.headers = {"Content-Type": "application/json"};
321
+
322
+ }
323
+ const response = await fetch(url, fetchOptions);
324
+
314
325
  clearTimeout(timeoutId);
315
326
 
316
327
  if (!response.ok) {
328
+ if (response.status === 401) {
329
+ const errorMessage = `Tu sesión ha expirado. `;
330
+ const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
331
+ const infrastructureError = $('<div>').addClass('error-section').html(errorIcon + `<p>${errorMessage}</p>`);
332
+ displayBotMessage(infrastructureError);
333
+ return null;
334
+ }
317
335
  try {
318
336
  // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
319
337
  const errorData = await response.json();
@@ -337,6 +355,14 @@ const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
337
355
  if (error.name === 'AbortError') {
338
356
  throw error; // Re-throw to be handled by handleChatMessage
339
357
  } else {
358
+ // Log detallado en consola
359
+ console.error('Error de red en callToolkit:', {
360
+ url,
361
+ method,
362
+ error,
363
+ message: error?.message,
364
+ stack: error?.stack,
365
+ });
340
366
  const friendlyMessage = "Ocurrió un error de red. Por favor, inténtalo de nuevo en unos momentos.";
341
367
  const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
342
368
  const commError = $('<div>').addClass('error-section').html(errorIcon + `<p>${friendlyMessage}</p>`);
@@ -111,8 +111,11 @@
111
111
  data-custom-fields='{{ prompt.custom_fields | tojson }}'>
112
112
  {{ prompt.description }}
113
113
  </a>
114
- </li> {% endfor %}
115
- {% if not loop.last %}<li><hr class="dropdown-divider"></li>{% endif %}
114
+ </li>
115
+ {% endfor %}
116
+ {% if not loop.last %}
117
+ <li><hr class="dropdown-divider"></li>
118
+ {% endif %}
116
119
  {% endfor %}
117
120
  {% endif %}
118
121
  </ul>
@@ -180,10 +183,11 @@
180
183
  // --- Global Configuration from Backend ---
181
184
  window.companyShortName = "{{ company_short_name }}";
182
185
  window.user_identifier = "{{ user_identifier }}";
186
+ window.redeemToken = "{{ redeem_token }}";
183
187
  window.iatoolkit_base_url = "{{ iatoolkit_base_url }}";
184
188
  window.availablePrompts = {{ prompts.message | tojson }};
185
- window.sendButtonColor = "{{ branding.send_button_color }}";
186
189
  window.onboardingCards = {{ onboarding_cards | tojson }};
190
+ window.sendButtonColor = "{{ branding.send_button_color }}";
187
191
  </script>
188
192
 
189
193
  <!-- Carga de los scripts JS externos después de definir las variables globales -->