iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__py3-none-any.whl

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 (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
@@ -1,96 +1,162 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
6
  from injector import inject
7
- from repositories.profile_repo import ProfileRepo
8
- from repositories.models import User, Company, ApiKey
7
+ from iatoolkit.repositories.profile_repo import ProfileRepo
8
+ from iatoolkit.services.i18n_service import I18nService
9
+ from iatoolkit.repositories.models import User, Company, ApiKey
9
10
  from flask_bcrypt import check_password_hash
10
- from common.session_manager import SessionManager
11
+ from iatoolkit.common.session_manager import SessionManager
12
+ from iatoolkit.services.dispatcher_service import Dispatcher
13
+ from iatoolkit.services.language_service import LanguageService
14
+ from iatoolkit.services.user_session_context_service import UserSessionContextService
15
+ from iatoolkit.services.configuration_service import ConfigurationService
11
16
  from flask_bcrypt import Bcrypt
12
- from infra.mail_app import MailApp
17
+ from iatoolkit.services.mail_service import MailService
13
18
  import random
14
- import logging
15
19
  import re
16
20
  import secrets
17
21
  import string
18
- from datetime import datetime, timezone
19
- from services.user_session_context_service import UserSessionContextService
20
- from services.query_service import QueryService
22
+ import logging
23
+ from typing import List, Dict
21
24
 
22
25
 
23
26
  class ProfileService:
24
27
  @inject
25
28
  def __init__(self,
29
+ i18n_service: I18nService,
26
30
  profile_repo: ProfileRepo,
27
31
  session_context_service: UserSessionContextService,
28
- query_service: QueryService,
29
- mail_app: MailApp):
32
+ config_service: ConfigurationService,
33
+ lang_service: LanguageService,
34
+ dispatcher: Dispatcher,
35
+ mail_service: MailService):
36
+ self.i18n_service = i18n_service
30
37
  self.profile_repo = profile_repo
38
+ self.dispatcher = dispatcher
31
39
  self.session_context = session_context_service
32
- self.query_service = query_service
33
- self.mail_app = mail_app
40
+ self.config_service = config_service
41
+ self.lang_service = lang_service
42
+ self.mail_service = mail_service
34
43
  self.bcrypt = Bcrypt()
35
44
 
36
-
37
45
  def login(self, company_short_name: str, email: str, password: str) -> dict:
38
46
  try:
39
- # check if exits
47
+ # check if user exists
40
48
  user = self.profile_repo.get_user_by_email(email)
41
49
  if not user:
42
- return {"error": "Usuario no encontrado"}
50
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
43
51
 
44
52
  # check the encrypted password
45
53
  if not check_password_hash(user.password, password):
46
- return {"error": "Contraseña inválida"}
54
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
47
55
 
48
- company = self.get_company_by_short_name(company_short_name)
56
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
49
57
  if not company:
50
- return {"error": "Empresa no encontrada"}
58
+ return {'success': False, "message": "missing company"}
51
59
 
52
- # check that user belongs to company
60
+ # check that user belongs to company
53
61
  if company not in user.companies:
54
- return {"error": "Usuario no esta autorizado para esta empresa"}
62
+ return {'success': False, "message": self.i18n_service.t('errors.services.user_not_authorized')}
55
63
 
56
64
  if not user.verified:
57
- return {"error": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
65
+ return {'success': False,
66
+ "message": self.i18n_service.t('errors.services.account_not_verified')}
67
+
68
+ user_role = self.profile_repo.get_user_role_in_company(company.id, user.id)
69
+
70
+ # 1. Build the local user profile dictionary here.
71
+ # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
72
+ user_identifier = user.email
73
+ user_profile = {
74
+ "user_email": user.email,
75
+ "user_fullname": f'{user.first_name} {user.last_name}',
76
+ "user_is_local": True,
77
+ "user_id": user.id,
78
+ "user_role": user_role,
79
+ "extras": {}
80
+ }
81
+
82
+ # 2. create user_profile in context
83
+ self.save_user_profile(company, user_identifier, user_profile)
84
+
85
+ # 3. create the web session
86
+ self.set_session_for_user(company.short_name, user_identifier)
87
+ return {'success': True, "user_identifier": user_identifier, "message": "Login ok"}
88
+ except Exception as e:
89
+ logging.error(f"Error in login: {e}")
90
+ return {'success': False, "message": str(e)}
58
91
 
59
- # clear history save user data into session manager
60
- self.set_user_session(user=user, company=company)
92
+ def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
93
+ """
94
+ Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
95
+ """
96
+ user_profile['company_short_name'] = company.short_name
97
+ user_profile['user_identifier'] = user_identifier
98
+ user_profile['id'] = user_identifier
99
+ user_profile['company_id'] = company.id
100
+ user_profile['company'] = company.name
101
+ user_profile['language'] = self.lang_service.get_current_language()
102
+
103
+ # save user_profile in Redis session
104
+ self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
105
+
106
+ def set_session_for_user(self, company_short_name: str, user_identifier:str ):
107
+ # save a min Flask session cookie for this user
108
+ SessionManager.set('company_short_name', company_short_name)
109
+ SessionManager.set('user_identifier', user_identifier)
110
+
111
+ def get_current_session_info(self) -> dict:
112
+ """
113
+ Gets the current web user's profile from the unified session.
114
+ This is the standard way to access user data for web requests.
115
+ """
116
+ # 1. Get identifiers from the simple Flask session cookie.
117
+ user_identifier = SessionManager.get('user_identifier')
118
+ company_short_name = SessionManager.get('company_short_name')
119
+
120
+ if not user_identifier or not company_short_name:
121
+ # No authenticated web user.
122
+ return {}
123
+
124
+ # 2. Use the identifiers to fetch the full, authoritative profile from Redis.
125
+ profile = self.session_context.get_profile_data(company_short_name, user_identifier)
126
+
127
+ return {
128
+ "user_identifier": user_identifier,
129
+ "company_short_name": company_short_name,
130
+ "profile": profile
131
+ }
61
132
 
62
- # initialize company context
63
- self.query_service.llm_init_context(
64
- company_short_name=company_short_name,
65
- local_user_id=user.id
66
- )
133
+ def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
134
+ """
135
+ Business logic to update a user's preferred language.
136
+ It validates the language and then calls the generic update method.
137
+ """
138
+ # 1. Validate that the language is supported by checking the loaded translations.
139
+ if new_lang not in self.i18n_service.translations:
140
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
67
141
 
68
- return {"message": "Login exitoso"}
142
+ try:
143
+ # 2. Call the generic update_user method, passing the specific field to update.
144
+ self.update_user(user_identifier, preferred_language=new_lang)
145
+ return {'success': True, 'message': 'Language updated successfully.'}
69
146
  except Exception as e:
70
- logging.exception(f"login error: {str(e)}")
71
- return {"error": str(e)}
72
-
73
- def set_user_session(self, user: User, company: Company):
74
- SessionManager.set('user_id', user.id)
75
- SessionManager.set('company_id', company.id)
76
- SessionManager.set('company_short_name', company.short_name)
77
-
78
- # save user data into session manager
79
- user_data = {
80
- "id": user.id,
81
- "email": user.email,
82
- "user_fullname": f'{user.first_name} {user.last_name}',
83
- "super_user": user.super_user,
84
- "company_id": company.id,
85
- "company": company.name,
86
- "company_short_name": company.short_name,
87
- "user_is_local": True, # origin of data
88
- "extras": {} # company specific data
89
- }
90
- SessionManager.set('user', user_data)
147
+ # Log the error and return a generic failure message.
148
+ logging.error(f"Failed to update language for {user_identifier}: {e}")
149
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
150
+
91
151
 
92
- # save time session was activated (in timestamp format)
93
- SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
152
+ def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
153
+ """
154
+ Fetches a user profile directly by their identifier, bypassing the Flask session.
155
+ This is ideal for API-side checks.
156
+ """
157
+ if not company_short_name or not user_identifier:
158
+ return {}
159
+ return self.session_context.get_profile_data(company_short_name, user_identifier)
94
160
 
95
161
 
96
162
  def signup(self,
@@ -98,19 +164,18 @@ class ProfileService:
98
164
  email: str,
99
165
  first_name: str,
100
166
  last_name: str,
101
- rut: str,
102
167
  password: str,
103
168
  confirm_password: str,
104
169
  verification_url: str) -> dict:
105
170
  try:
106
171
 
107
172
  # get company info
108
- company = self.get_company_by_short_name(company_short_name)
173
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
109
174
  if not company:
110
- return {"error": f"la empresa {company_short_name} no existe"}
175
+ return {
176
+ "error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
111
177
 
112
178
  # normalize format's
113
- rut = rut.lower().replace(" ", "")
114
179
  email = email.lower()
115
180
 
116
181
  # check if user exists
@@ -118,52 +183,59 @@ class ProfileService:
118
183
  if existing_user:
119
184
  # validate password
120
185
  if not self.bcrypt.check_password_hash(existing_user.password, password):
121
- return {"error": "La contraseña es incorrecta. No se puede agregar a la nueva empresa."}
122
-
123
- if rut != existing_user.rut:
124
- return {"error": "El RUT ingresado no corresponde al email existente."}
186
+ return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
125
187
 
126
188
  # check if register
127
189
  if company in existing_user.companies:
128
- return {"error": "Usuario ya registrado en esta empresa"}
190
+ return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
129
191
  else:
130
192
  # add new company to existing user
131
193
  existing_user.companies.append(company)
132
194
  self.profile_repo.save_user(existing_user)
133
- return {"message": "Usuario asociado a nueva empresa"}
195
+ return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
134
196
 
135
197
  # add the new user
136
198
  if password != confirm_password:
137
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
199
+ return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
138
200
 
139
201
  is_valid, message = self.validate_password(password)
140
202
  if not is_valid:
141
- return {"error": message}
203
+ # Translate the key returned by validate_password
204
+ return {"error": self.i18n_service.t(message)}
142
205
 
143
206
  # encrypt the password
144
207
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
145
208
 
209
+ # account verification can be skiped with this security parameter
210
+ verified = False
211
+ cfg = self.config_service.get_configuration(company_short_name, 'parameters')
212
+ if cfg and not cfg.get('verify_account', True):
213
+ verified = True
214
+ message = self.i18n_service.t('flash_messages.signup_success_no_verification')
215
+
146
216
  # create the new user
147
217
  new_user = User(email=email,
148
- rut=rut,
149
218
  password=hashed_password,
150
219
  first_name=first_name.lower(),
151
220
  last_name=last_name.lower(),
152
- verified=False,
221
+ verified=verified,
153
222
  verification_url=verification_url
154
223
  )
155
224
 
156
225
  # associate new company to user
157
226
  new_user.companies.append(company)
158
227
 
228
+ # and create in the database
159
229
  self.profile_repo.create_user(new_user)
160
230
 
161
231
  # send email with verification
162
- self.send_verification_email(new_user, company_short_name)
232
+ if not cfg or cfg.get('verify_account', True):
233
+ self.send_verification_email(new_user, company_short_name)
234
+ message = self.i18n_service.t('flash_messages.signup_success')
163
235
 
164
- return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
236
+ return {"message": message}
165
237
  except Exception as e:
166
- return {"error": str(e)}
238
+ return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
167
239
 
168
240
  def update_user(self, email: str, **kwargs) -> User:
169
241
  return self.profile_repo.update_user(email, **kwargs)
@@ -173,14 +245,14 @@ class ProfileService:
173
245
  # check if user exist
174
246
  user = self.profile_repo.get_user_by_email(email)
175
247
  if not user:
176
- return {"error": "El usuario no existe."}
248
+ return {"error": self.i18n_service.t('errors.verification.user_not_found')}
177
249
 
178
250
  # activate the user account
179
251
  self.profile_repo.verify_user(email)
180
- return {"message": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
252
+ return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
181
253
 
182
254
  except Exception as e:
183
- return {"error": str(e)}
255
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
184
256
 
185
257
  def change_password(self,
186
258
  email: str,
@@ -189,65 +261,61 @@ class ProfileService:
189
261
  confirm_password: str):
190
262
  try:
191
263
  if new_password != confirm_password:
192
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
264
+ return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
193
265
 
194
266
  # check the temporary code
195
267
  user = self.profile_repo.get_user_by_email(email)
196
268
  if not user or user.temp_code != temp_code:
197
- return {"error": "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."}
269
+ return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
198
270
 
199
271
  # encrypt and save the password, make the temporary code invalid
200
272
  hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
201
273
  self.profile_repo.update_password(email, hashed_password)
202
274
  self.profile_repo.reset_temp_code(email)
203
275
 
204
- return {"message": "La clave se cambio correctamente"}
276
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
205
277
  except Exception as e:
206
- return {"error": str(e)}
278
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
207
279
 
208
- def forgot_password(self, email: str, reset_url: str):
280
+ def forgot_password(self, company_short_name: str, email: str, reset_url: str):
209
281
  try:
210
282
  # Verificar si el usuario existe
211
283
  user = self.profile_repo.get_user_by_email(email)
212
284
  if not user:
213
- return {"error": "El usuario no existe."}
285
+ return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
214
286
 
215
287
  # Gen a temporary code and store in the repositories
216
288
  temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
217
289
  self.profile_repo.set_temp_code(email, temp_code)
218
290
 
219
291
  # send email to the user
220
- self.send_forgot_password_email(user, reset_url)
292
+ self.send_forgot_password_email(company_short_name, user, reset_url)
221
293
 
222
- return {"message": "se envio mail para cambio de clave"}
294
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
223
295
  except Exception as e:
224
- return {"error": str(e)}
296
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
225
297
 
226
298
  def validate_password(self, password):
227
299
  """
228
- Valida que una contraseña cumpla con los siguientes requisitos:
229
- - Al menos 8 caracteres de longitud
230
- - Contiene al menos una letra mayúscula
231
- - Contiene al menos una letra minúscula
232
- - Contiene al menos un número
233
- - Contiene al menos un carácter especial
300
+ Validates that a password meets all requirements.
301
+ Returns (True, "...") on success, or (False, "translation.key") on failure.
234
302
  """
235
303
  if len(password) < 8:
236
- return False, "La contraseña debe tener al menos 8 caracteres."
304
+ return False, "errors.validation.password_too_short"
237
305
 
238
306
  if not any(char.isupper() for char in password):
239
- return False, "La contraseña debe tener al menos una letra mayúscula."
307
+ return False, "errors.validation.password_no_uppercase"
240
308
 
241
309
  if not any(char.islower() for char in password):
242
- return False, "La contraseña debe tener al menos una letra minúscula."
310
+ return False, "errors.validation.password_no_lowercase"
243
311
 
244
312
  if not any(char.isdigit() for char in password):
245
- return False, "La contraseña debe tener al menos un número."
313
+ return False, "errors.validation.password_no_digit"
246
314
 
247
315
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
248
- return False, "La contraseña debe tener al menos un carácter especial."
316
+ return False, "errors.validation.password_no_special_char"
249
317
 
250
- return True, "La contraseña es válida."
318
+ return True, "Password is valid."
251
319
 
252
320
  def get_companies(self):
253
321
  return self.profile_repo.get_companies()
@@ -255,10 +323,35 @@ class ProfileService:
255
323
  def get_company_by_short_name(self, short_name: str) -> Company:
256
324
  return self.profile_repo.get_company_by_short_name(short_name)
257
325
 
326
+ def get_company_users(self, company_short_name: str) -> List[Dict]:
327
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
328
+ if not company:
329
+ return []
330
+
331
+ # get the company users from the repo
332
+ company_users = self.profile_repo.get_company_users_with_details(company_short_name)
333
+
334
+ users_data = []
335
+ for user, role, last_access in company_users:
336
+ users_data.append({
337
+ "first_name": user.first_name,
338
+ "last_name": user.last_name,
339
+ "email": user.email,
340
+ "created": user.created_at,
341
+ "verified": user.verified,
342
+ "role": role or "user",
343
+ "last_access": last_access
344
+ })
345
+
346
+ return users_data
347
+
348
+ def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
349
+ return self.profile_repo.get_active_api_key_entry(api_key_value)
350
+
258
351
  def new_api_key(self, company_short_name: str):
259
352
  company = self.get_company_by_short_name(company_short_name)
260
353
  if not company:
261
- return {"error": f"la empresa {company_short_name} no existe"}
354
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
262
355
 
263
356
  length = 40 # lenght of the api key
264
357
  alphabet = string.ascii_letters + string.digits
@@ -319,9 +412,12 @@ class ProfileService:
319
412
  </body>
320
413
  </html>
321
414
  """
322
- self.mail_app.send_email(to=new_user.email, subject=subject, body=body)
415
+ self.mail_service.send_mail(company_short_name=company_short_name,
416
+ recipient=new_user.email,
417
+ subject=subject,
418
+ body=body)
323
419
 
324
- def send_forgot_password_email(self, user: User, reset_url: str):
420
+ def send_forgot_password_email(self, company_short_name: str, user: User, reset_url: str):
325
421
  # send email to the user
326
422
  subject = f"Recuperación de Contraseña "
327
423
  body = f"""
@@ -372,5 +468,8 @@ class ProfileService:
372
468
  </html>
373
469
  """
374
470
 
375
- self.mail_app.send_email(to=user.email, subject=subject, body=body)
376
- return {"message": "se envio mail para cambio de clave"}
471
+ self.mail_service.send_mail(company_short_name=company_short_name,
472
+ recipient=user.email,
473
+ subject=subject,
474
+ body=body)
475
+ return {"message": self.i18n_service.t('services.mail_change_password') }