iatoolkit 0.56.0__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 (119) hide show
  1. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/PKG-INFO +1 -1
  2. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/pyproject.toml +1 -1
  3. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/iatoolkit.py +1 -1
  4. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/models.py +26 -1
  5. iatoolkit-0.57.0/src/iatoolkit/services/auth_service.py +181 -0
  6. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/base_login_view.py +3 -0
  7. iatoolkit-0.57.0/src/iatoolkit/views/external_login_view.py +67 -0
  8. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/login_view.py +4 -3
  9. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/PKG-INFO +1 -1
  10. iatoolkit-0.56.0/src/iatoolkit/services/auth_service.py +0 -77
  11. iatoolkit-0.56.0/src/iatoolkit/views/external_login_view.py +0 -110
  12. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/readme.md +0 -0
  13. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/requirements.txt +0 -0
  14. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/setup.cfg +0 -0
  15. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/__init__.py +0 -0
  16. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/base_company.py +0 -0
  17. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/cli_commands.py +0 -0
  18. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/common/__init__.py +0 -0
  19. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/common/exceptions.py +0 -0
  20. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/common/routes.py +0 -0
  21. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/common/session_manager.py +0 -0
  22. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/common/util.py +0 -0
  23. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/company_registry.py +0 -0
  24. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/__init__.py +0 -0
  25. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/call_service.py +0 -0
  26. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  27. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  28. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  29. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  30. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  31. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
  32. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  33. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/gemini_adapter.py +0 -0
  34. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
  35. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_client.py +0 -0
  36. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
  37. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/llm_response.py +0 -0
  38. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/mail_app.py +0 -0
  39. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/openai_adapter.py +0 -0
  40. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  41. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/__init__.py +0 -0
  42. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/database_manager.py +0 -0
  43. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/document_repo.py +0 -0
  44. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
  45. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
  46. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/tasks_repo.py +0 -0
  47. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
  48. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/__init__.py +0 -0
  49. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/benchmark_service.py +0 -0
  50. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/branding_service.py +0 -0
  51. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
  52. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/document_service.py +0 -0
  53. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/excel_service.py +0 -0
  54. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/file_processor_service.py +0 -0
  55. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/history_service.py +0 -0
  56. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/jwt_service.py +0 -0
  57. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/load_documents_service.py +0 -0
  58. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/mail_service.py +0 -0
  59. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/onboarding_service.py +0 -0
  60. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/profile_service.py +0 -0
  61. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/prompt_manager_service.py +0 -0
  62. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/query_service.py +0 -0
  63. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/search_service.py +0 -0
  64. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/sql_service.py +0 -0
  65. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/tasks_service.py +0 -0
  66. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
  67. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
  68. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  69. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_feedback.js +0 -0
  70. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  71. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_history.js +0 -0
  72. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_main.js +0 -0
  73. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/js/chat_onboarding.js +0 -0
  74. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  75. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_info.css +0 -0
  76. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  77. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
  78. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
  79. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
  80. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/format_styles.prompt +0 -0
  81. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/query_main.prompt +0 -0
  82. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/system_prompts/sql_rules.prompt +0 -0
  83. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/_branding_styles.html +0 -0
  84. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/_login_widget.html +0 -0
  85. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/_navbar.html +0 -0
  86. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/about.html +0 -0
  87. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/base.html +0 -0
  88. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/change_password.html +0 -0
  89. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/chat.html +0 -0
  90. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/chat_modals.html +0 -0
  91. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/error.html +0 -0
  92. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/forgot_password.html +0 -0
  93. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/header.html +0 -0
  94. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/index.html +0 -0
  95. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/login_simulation.html +0 -0
  96. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
  97. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/signup.html +0 -0
  98. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/templates/test.html +0 -0
  99. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/__init__.py +0 -0
  100. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/change_password_view.py +0 -0
  101. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/chat_token_request_view.py +0 -0
  102. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/file_store_api_view.py +0 -0
  103. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
  104. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/history_api_view.py +0 -0
  105. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/index_view.py +0 -0
  106. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
  107. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
  108. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/llmquery_web_view.py +0 -0
  109. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/login_simulation_view.py +0 -0
  110. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/prompt_api_view.py +0 -0
  111. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/signup_view.py +0 -0
  112. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/tasks_review_view.py +0 -0
  113. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/tasks_view.py +0 -0
  114. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  115. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit/views/verify_user_view.py +0 -0
  116. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/SOURCES.txt +0 -0
  117. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  118. {iatoolkit-0.56.0 → iatoolkit-0.57.0}/src/iatoolkit.egg-info/requires.txt +0 -0
  119. {iatoolkit-0.56.0 → 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.56.0
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.56.0"
7
+ version = "0.57.0"
8
8
  requires-python = ">=3.12"
9
9
  description = "IAToolkit"
10
10
  readme = "readme.md"
@@ -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()
@@ -8,6 +8,7 @@ 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
@@ -23,6 +24,7 @@ class BaseLoginView(MethodView):
23
24
  @inject
24
25
  def __init__(self,
25
26
  profile_service: ProfileService,
27
+ auth_service: AuthService,
26
28
  jwt_service: JWTService,
27
29
  branding_service: BrandingService,
28
30
  prompt_service: PromptService,
@@ -30,6 +32,7 @@ class BaseLoginView(MethodView):
30
32
  query_service: QueryService
31
33
  ):
32
34
  self.profile_service = profile_service
35
+ self.auth_service = auth_service
33
36
  self.jwt_service = jwt_service
34
37
  self.branding_service = branding_service
35
38
  self.prompt_service = prompt_service
@@ -0,0 +1,67 @@
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
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_session(company, user_identifier)
41
+
42
+ # 3. Delegate the path decision to the centralized logic.
43
+ try:
44
+ return self._handle_login_path(company_short_name, user_identifier, company)
45
+ except Exception as e:
46
+ logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
47
+ return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
48
+
49
+
50
+ class RedeemTokenApiView(BaseLoginView):
51
+ # this endpoint is only used ONLY by chat_main.js to redeem a chat token
52
+ def post(self, company_short_name: str):
53
+ data = request.get_json()
54
+ if not data or 'token' not in data:
55
+ return jsonify({"error": "Falta token de validación"}), 400
56
+
57
+ # get the token and validate with auth service
58
+ token = data.get('token')
59
+ redeem_result = self.auth_service.redeem_token_for_session(
60
+ company_short_name=company_short_name,
61
+ token=token
62
+ )
63
+
64
+ if not redeem_result['success']:
65
+ return {"error": redeem_result['error']}, 401
66
+
67
+ return {"status": "ok"}, 200
@@ -7,6 +7,7 @@ from flask.views import MethodView
7
7
  from flask import request, redirect, render_template, url_for
8
8
  from injector import inject
9
9
  from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.auth_service import AuthService
10
11
  from iatoolkit.services.jwt_service import JWTService
11
12
  from iatoolkit.services.query_service import QueryService
12
13
  from iatoolkit.services.prompt_manager_service import PromptService
@@ -21,7 +22,6 @@ class LoginView(BaseLoginView):
21
22
  Handles login for local users.
22
23
  Authenticates and then delegates the path decision (fast/slow) to the base class.
23
24
  """
24
-
25
25
  def post(self, company_short_name: str):
26
26
  company = self.profile_service.get_company_by_short_name(company_short_name)
27
27
  if not company:
@@ -30,8 +30,8 @@ class LoginView(BaseLoginView):
30
30
  email = request.form.get('email')
31
31
  password = request.form.get('password')
32
32
 
33
- # 1. Authenticate user and create the session for internal user
34
- auth_response = self.profile_service.login(
33
+ # 1. Authenticate internal user
34
+ auth_response = self.auth_service.login_local_user(
35
35
  company_short_name=company_short_name,
36
36
  email=email,
37
37
  password=password
@@ -67,6 +67,7 @@ class FinalizeContextView(MethodView):
67
67
  @inject
68
68
  def __init__(self,
69
69
  profile_service: ProfileService,
70
+ auth_service: AuthService,
70
71
  query_service: QueryService,
71
72
  prompt_service: PromptService,
72
73
  branding_service: BrandingService,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 0.56.0
3
+ Version: 0.57.0
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -1,77 +0,0 @@
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 flask import request
10
- import logging
11
-
12
-
13
- class AuthService:
14
- """
15
- Centralized service for handling authentication for all incoming requests.
16
- It determines the user's identity based on either a Flask session cookie or an API Key.
17
- """
18
-
19
- @inject
20
- def __init__(self, profile_service: ProfileService):
21
- """
22
- Injects ProfileService to access session information and validate API keys.
23
- """
24
- self.profile_service = profile_service
25
-
26
- def verify(self) -> dict:
27
- """
28
- Verifies the current request and identifies the user.
29
-
30
- Returns a dictionary with:
31
- - success: bool
32
- - user_identifier: str (if successful)
33
- - company_short_name: str (if successful)
34
- - error_message: str (on failure)
35
- - status_code: int (on failure)
36
- """
37
- # --- Priority 1: Check for a valid Flask web session ---
38
- session_info = self.profile_service.get_current_session_info()
39
- if session_info and session_info.get('user_identifier'):
40
- # User is authenticated via a web session cookie.
41
- return {
42
- "success": True,
43
- "company_short_name": session_info['company_short_name'],
44
- "user_identifier": session_info['user_identifier']
45
- }
46
-
47
- # --- Priority 2: Check for a valid API Key in headers ---
48
- api_key = None
49
- auth = request.headers.get('Authorization', '')
50
- if isinstance(auth, str) and auth.lower().startswith('bearer '):
51
- api_key = auth.split(' ', 1)[1].strip()
52
-
53
- if api_key:
54
- api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
55
- if not api_key_entry:
56
- logging.info(f"Invalid or inactive API Key {api_key}")
57
- return {"success": False, "error": "Invalid or inactive API Key", "status_code": 401}
58
-
59
- # obtain the company from the api_key_entry
60
- company = api_key_entry.company
61
-
62
- # For API calls, the external_user_id must be provided in the request.
63
- user_identifier = ''
64
- if request.is_json:
65
- data = request.get_json() or {}
66
- user_identifier = data.get('user_identifier', '')
67
-
68
- return {
69
- "success": True,
70
- "company_short_name": company.short_name,
71
- "user_identifier": user_identifier
72
- }
73
-
74
- # --- Failure: No valid credentials found ---
75
- logging.info(f"Authentication required. No session cookie or API Key provided.")
76
- return {"success": False, "error": "Authentication required. No session cookie or API Key provided.",
77
- "status_code": 402}
@@ -1,110 +0,0 @@
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
9
- from flask.views import MethodView
10
- from injector import inject
11
- from iatoolkit.services.auth_service import AuthService
12
- from iatoolkit.views.base_login_view import BaseLoginView
13
-
14
- # Importar los servicios que necesita la clase base
15
- from iatoolkit.services.profile_service import ProfileService
16
- from iatoolkit.services.jwt_service import JWTService
17
- from iatoolkit.services.branding_service import BrandingService
18
- from iatoolkit.services.onboarding_service import OnboardingService
19
- from iatoolkit.services.query_service import QueryService
20
- from iatoolkit.services.prompt_manager_service import PromptService
21
-
22
- class ExternalLoginView(BaseLoginView):
23
- """
24
- Handles login for external users via API.
25
- Authenticates and then delegates the path decision (fast/slow) to the base class.
26
- """
27
- @inject
28
- def __init__(self,
29
- iauthentication: AuthService,
30
- jwt_service: JWTService,
31
- profile_service: ProfileService,
32
- branding_service: BrandingService,
33
- prompt_service: PromptService,
34
- onboarding_service: OnboardingService,
35
- query_service: QueryService):
36
- # Pass the dependencies for the base class to its __init__
37
- super().__init__(
38
- profile_service=profile_service,
39
- jwt_service=jwt_service,
40
- branding_service=branding_service,
41
- onboarding_service=onboarding_service,
42
- query_service=query_service,
43
- prompt_service=prompt_service,
44
- )
45
- # Handle the dependency specific to this child class
46
- self.iauthentication = iauthentication
47
-
48
- def post(self, company_short_name: str):
49
- data = request.get_json()
50
- if not data or 'user_identifier' not in data:
51
- return jsonify({"error": "Falta user_identifier"}), 400
52
-
53
- company = self.profile_service.get_company_by_short_name(company_short_name)
54
- if not company:
55
- return jsonify({"error": "Empresa no encontrada"}), 404
56
-
57
- user_identifier = data.get('user_identifier')
58
- if not user_identifier:
59
- return jsonify({"error": "missing user_identifier"}), 404
60
-
61
- # 1. Authenticate the API call.
62
- iaut = self.iauthentication.verify()
63
- if not iaut.get("success"):
64
- return jsonify(iaut), 401
65
-
66
- # 2. Create the external user session.
67
- self.profile_service.create_external_user_session(company, user_identifier)
68
-
69
- # 3. Delegate the path decision to the centralized logic.
70
- try:
71
- return self._handle_login_path(company_short_name, user_identifier, company)
72
- except Exception as e:
73
- logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
74
- return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
75
-
76
-
77
- class RedeemTokenApiView(MethodView):
78
- # this endpoint is only used ONLY by chat_main.js to redeem a chat token
79
- @inject
80
- def __init__(self,
81
- profile_service: ProfileService,
82
- jwt_service: JWTService):
83
- self.profile_service = profile_service
84
- self.jwt_service = jwt_service
85
-
86
- def post(self, company_short_name: str):
87
- data = request.get_json()
88
- if not data or 'token' not in data:
89
- return jsonify({"error": "Falta token de validación"}), 400
90
-
91
- # 1. validate the token
92
- token = data.get('token')
93
- payload = self.jwt_service.validate_chat_jwt(token)
94
- if not payload:
95
- logging.warning("Intento de canjear un token inválido o expirado.")
96
- return {"error": "Token inválido o expirado."}, 401
97
-
98
- # 2. if token is valid, extract the user_identifier
99
- user_identifier = payload.get('user_identifier')
100
-
101
- try:
102
- # 3. create the Flask session
103
- self.profile_service.set_session_for_user(company_short_name, user_identifier)
104
- logging.info(f"Token de sesión canjeado exitosamente para {user_identifier}.")
105
-
106
- return {"status": "ok"}, 200
107
-
108
- except Exception as e:
109
- logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
110
- return {"error": "No se pudo crear la sesión del usuario."}, 500
File without changes
File without changes
File without changes