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.

Files changed (119) hide show
  1. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/PKG-INFO +1 -1
  2. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/pyproject.toml +1 -1
  3. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/routes.py +2 -8
  4. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/iatoolkit.py +1 -1
  5. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/models.py +26 -1
  6. iatoolkit-0.58.0/src/iatoolkit/services/auth_service.py +181 -0
  7. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/profile_service.py +10 -10
  8. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/query_service.py +0 -3
  9. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/user_session_context_service.py +1 -1
  10. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_main.js +2 -2
  11. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_login_widget.html +2 -1
  12. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/chat.html +3 -63
  13. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/base_login_view.py +14 -29
  14. iatoolkit-0.58.0/src/iatoolkit/views/external_login_view.py +83 -0
  15. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/init_context_api_view.py +2 -1
  16. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/llmquery_api_view.py +0 -6
  17. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/login_view.py +10 -4
  18. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/PKG-INFO +1 -1
  19. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/SOURCES.txt +0 -1
  20. iatoolkit-0.56.0/src/iatoolkit/services/auth_service.py +0 -77
  21. iatoolkit-0.56.0/src/iatoolkit/views/external_login_view.py +0 -110
  22. iatoolkit-0.56.0/src/iatoolkit/views/llmquery_web_view.py +0 -38
  23. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/readme.md +0 -0
  24. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/requirements.txt +0 -0
  25. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/setup.cfg +0 -0
  26. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/__init__.py +0 -0
  27. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/base_company.py +0 -0
  28. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/cli_commands.py +0 -0
  29. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/__init__.py +0 -0
  30. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/exceptions.py +0 -0
  31. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/session_manager.py +0 -0
  32. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/common/util.py +0 -0
  33. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/company_registry.py +0 -0
  34. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/__init__.py +0 -0
  35. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/call_service.py +0 -0
  36. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  37. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  38. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  39. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  40. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  41. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
  42. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  43. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/gemini_adapter.py +0 -0
  44. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
  45. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_client.py +0 -0
  46. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
  47. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/llm_response.py +0 -0
  48. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/mail_app.py +0 -0
  49. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/openai_adapter.py +0 -0
  50. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  51. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/__init__.py +0 -0
  52. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/database_manager.py +0 -0
  53. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/document_repo.py +0 -0
  54. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
  55. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
  56. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/tasks_repo.py +0 -0
  57. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
  58. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/__init__.py +0 -0
  59. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/benchmark_service.py +0 -0
  60. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/branding_service.py +0 -0
  61. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
  62. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/document_service.py +0 -0
  63. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/excel_service.py +0 -0
  64. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/file_processor_service.py +0 -0
  65. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/history_service.py +0 -0
  66. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/jwt_service.py +0 -0
  67. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/load_documents_service.py +0 -0
  68. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/mail_service.py +0 -0
  69. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/onboarding_service.py +0 -0
  70. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/prompt_manager_service.py +0 -0
  71. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/search_service.py +0 -0
  72. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/sql_service.py +0 -0
  73. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/tasks_service.py +0 -0
  74. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
  75. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  76. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_feedback.js +0 -0
  77. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  78. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_history.js +0 -0
  79. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/js/chat_onboarding.js +0 -0
  80. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  81. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_info.css +0 -0
  82. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  83. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
  84. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
  85. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
  86. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/format_styles.prompt +0 -0
  87. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/query_main.prompt +0 -0
  88. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/system_prompts/sql_rules.prompt +0 -0
  89. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_branding_styles.html +0 -0
  90. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/_navbar.html +0 -0
  91. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/about.html +0 -0
  92. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/base.html +0 -0
  93. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/change_password.html +0 -0
  94. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/chat_modals.html +0 -0
  95. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/error.html +0 -0
  96. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/forgot_password.html +0 -0
  97. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/header.html +0 -0
  98. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/index.html +0 -0
  99. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/login_simulation.html +0 -0
  100. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
  101. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/signup.html +0 -0
  102. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/templates/test.html +0 -0
  103. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/__init__.py +0 -0
  104. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/change_password_view.py +0 -0
  105. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/chat_token_request_view.py +0 -0
  106. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/file_store_api_view.py +0 -0
  107. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
  108. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/history_api_view.py +0 -0
  109. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/index_view.py +0 -0
  110. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/login_simulation_view.py +0 -0
  111. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/prompt_api_view.py +0 -0
  112. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/signup_view.py +0 -0
  113. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/tasks_review_view.py +0 -0
  114. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/tasks_view.py +0 -0
  115. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  116. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit/views/verify_user_view.py +0 -0
  117. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  118. {iatoolkit-0.56.0 → iatoolkit-0.58.0}/src/iatoolkit.egg-info/requires.txt +0 -0
  119. {iatoolkit-0.56.0 → iatoolkit-0.58.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.56.0
3
+ Version: 0.58.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.56.0"
7
+ version = "0.58.0"
8
8
  requires-python = ">=3.12"
9
9
  description = "IAToolkit"
10
10
  readme = "readme.md"
@@ -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/init_context_api',
71
- view_func=InitContextApiView.as_view('init_context_api'))
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.56.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. Call the session creation helper with the pre-built profile.
70
- # user_identifier = str(user.id)
71
- self.create_web_session(company, user_identifier, user_profile)
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 create_external_user_session(self, company: Company, user_identifier: str):
78
+ def create_external_user_profile_context(self, company: Company, user_identifier: str):
77
79
  """
78
- Public method for views to create a web session for an external user.
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.create_web_session(
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 create_web_session(self, company: Company, user_identifier: str, user_profile: dict):
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()
@@ -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 de sesión para un usuario de forma atómica."""
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" required>
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 = "{{ redeem_token }}";
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, company_short_name: str, user_identifier: str, company):
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=company_short_name, user_identifier=user_identifier
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