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.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {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
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
19
|
-
from
|
|
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
|
-
|
|
29
|
-
|
|
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.
|
|
33
|
-
self.
|
|
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
|
|
47
|
+
# check if user exists
|
|
40
48
|
user = self.profile_repo.get_user_by_email(email)
|
|
41
49
|
if not user:
|
|
42
|
-
return {
|
|
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 {
|
|
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 {"
|
|
58
|
+
return {'success': False, "message": "missing company"}
|
|
51
59
|
|
|
52
|
-
# check that user belongs to
|
|
60
|
+
# check that user belongs to company
|
|
53
61
|
if company not in user.companies:
|
|
54
|
-
return {"
|
|
62
|
+
return {'success': False, "message": self.i18n_service.t('errors.services.user_not_authorized')}
|
|
55
63
|
|
|
56
64
|
if not user.verified:
|
|
57
|
-
return {
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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 {
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
252
|
+
return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
|
|
181
253
|
|
|
182
254
|
except Exception as e:
|
|
183
|
-
return {"error":
|
|
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":
|
|
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":
|
|
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":
|
|
276
|
+
return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
|
|
205
277
|
except Exception as e:
|
|
206
|
-
return {"error":
|
|
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":
|
|
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":
|
|
294
|
+
return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
|
|
223
295
|
except Exception as e:
|
|
224
|
-
return {"error":
|
|
296
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
225
297
|
|
|
226
298
|
def validate_password(self, password):
|
|
227
299
|
"""
|
|
228
|
-
|
|
229
|
-
|
|
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, "
|
|
304
|
+
return False, "errors.validation.password_too_short"
|
|
237
305
|
|
|
238
306
|
if not any(char.isupper() for char in password):
|
|
239
|
-
return False, "
|
|
307
|
+
return False, "errors.validation.password_no_uppercase"
|
|
240
308
|
|
|
241
309
|
if not any(char.islower() for char in password):
|
|
242
|
-
return False, "
|
|
310
|
+
return False, "errors.validation.password_no_lowercase"
|
|
243
311
|
|
|
244
312
|
if not any(char.isdigit() for char in password):
|
|
245
|
-
return False, "
|
|
313
|
+
return False, "errors.validation.password_no_digit"
|
|
246
314
|
|
|
247
315
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
248
|
-
return False, "
|
|
316
|
+
return False, "errors.validation.password_no_special_char"
|
|
249
317
|
|
|
250
|
-
return True, "
|
|
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":
|
|
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.
|
|
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.
|
|
376
|
-
|
|
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') }
|