trovesuite 1.0.1__py3-none-any.whl → 1.0.31__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.
- trovesuite/__init__.py +12 -5
- trovesuite/auth/__init__.py +2 -5
- trovesuite/auth/auth_controller.py +6 -5
- trovesuite/auth/auth_read_dto.py +3 -3
- trovesuite/auth/auth_service.py +223 -80
- trovesuite/auth/auth_write_dto.py +1 -1
- trovesuite/configs/database.py +212 -58
- trovesuite/configs/settings.py +75 -132
- trovesuite/entities/health.py +4 -4
- trovesuite/notification/__init__.py +14 -0
- trovesuite/notification/notification_base.py +13 -0
- trovesuite/notification/notification_controller.py +21 -0
- trovesuite/notification/notification_read_dto.py +21 -0
- trovesuite/notification/notification_service.py +73 -0
- trovesuite/notification/notification_write_dto.py +21 -0
- trovesuite/storage/__init__.py +42 -0
- trovesuite/storage/storage_base.py +63 -0
- trovesuite/storage/storage_controller.py +198 -0
- trovesuite/storage/storage_read_dto.py +74 -0
- trovesuite/storage/storage_service.py +529 -0
- trovesuite/storage/storage_write_dto.py +70 -0
- trovesuite/utils/__init__.py +3 -1
- trovesuite/utils/helper.py +714 -5
- trovesuite/utils/templates.py +487 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/METADATA +184 -9
- trovesuite-1.0.31.dist-info/RECORD +34 -0
- trovesuite-1.0.1.dist-info/RECORD +0 -21
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/top_level.txt +0 -0
trovesuite/utils/helper.py
CHANGED
|
@@ -1,13 +1,156 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import List
|
|
1
|
+
from datetime import datetime, timedelta, timezone, date, time
|
|
2
|
+
from typing import Dict, List, Optional, Callable
|
|
3
|
+
from ..configs.database import DatabaseManager
|
|
4
|
+
from ..configs.settings import db_settings
|
|
5
|
+
from ..configs.logging import get_logger
|
|
3
6
|
from typing import TypeVar
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
import hashlib
|
|
8
|
+
import uuid
|
|
9
|
+
import jwt
|
|
10
|
+
import json
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from .templates import (
|
|
13
|
+
RESOURCE_STATUS_CHANGE_HTML_TEMPLATE,
|
|
14
|
+
RESOURCE_STATUS_CHANGE_TEXT_TEMPLATE,
|
|
15
|
+
)
|
|
16
|
+
import random
|
|
17
|
+
from ..notification import NotificationService
|
|
18
|
+
from ..notification import NotificationEmailServiceWriteDto
|
|
6
19
|
|
|
7
20
|
T = TypeVar("T")
|
|
8
21
|
|
|
22
|
+
logger = get_logger("helper")
|
|
23
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
|
24
|
+
|
|
9
25
|
class Helper:
|
|
10
26
|
|
|
27
|
+
@staticmethod
|
|
28
|
+
def generate_otp(length: int = 6) -> str:
|
|
29
|
+
"""Generate a random numeric OTP of a given length."""
|
|
30
|
+
return ''.join([str(random.randint(0, 9)) for _ in range(length)])
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def current_date_time():
|
|
34
|
+
|
|
35
|
+
if 11 <= int(datetime.now().day) <= 13:
|
|
36
|
+
suffix = "th"
|
|
37
|
+
|
|
38
|
+
last_digit = int(datetime.now().day) % 10
|
|
39
|
+
|
|
40
|
+
if last_digit == 1:
|
|
41
|
+
suffix = "st"
|
|
42
|
+
|
|
43
|
+
elif last_digit == 2:
|
|
44
|
+
suffix = "nd"
|
|
45
|
+
|
|
46
|
+
elif last_digit == 3:
|
|
47
|
+
suffix = "rd"
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
suffix = "th"
|
|
51
|
+
|
|
52
|
+
now = datetime.now()
|
|
53
|
+
day = now.day
|
|
54
|
+
|
|
55
|
+
# In helper.py
|
|
56
|
+
cdatetime = datetime.now().replace(microsecond=0, second=0)
|
|
57
|
+
cdate = now.strftime(f"%A {day}{suffix} %B, %Y")
|
|
58
|
+
ctime = now.strftime("%I:%M %p")
|
|
59
|
+
|
|
60
|
+
return {"cdate": cdate, "ctime": ctime, "cdatetime": cdatetime}
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def generate_unique_identifier(prefix: str):
|
|
64
|
+
|
|
65
|
+
max_length = 63
|
|
66
|
+
reserved = len(prefix) + len("_")
|
|
67
|
+
|
|
68
|
+
_uuid = f"{uuid.uuid4()}-{uuid.uuid1()}"
|
|
69
|
+
hash_digest = hashlib.sha256(_uuid.encode()).hexdigest()
|
|
70
|
+
|
|
71
|
+
return f"{prefix}_{hash_digest[:max_length - reserved]}"
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def generate_unique_resource_identifier(
|
|
75
|
+
prefix: str,
|
|
76
|
+
tenant_id: Optional[str],
|
|
77
|
+
extra_check_functions: Optional[List[Callable[[str], Optional[int]]]] = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Generate a unique identifier that does not already exist in the resource_ids table.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
prefix: Prefix for the identifier (e.g., 'grp', 'uid').
|
|
83
|
+
tenant_id: Tenant schema name. If None, the main resource_ids table is checked.
|
|
84
|
+
extra_check_functions: Optional list of callables that receive the candidate
|
|
85
|
+
identifier and return a truthy value if the identifier already exists in
|
|
86
|
+
another table that should be considered.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A unique identifier that does not exist in the resource_ids table (and passes
|
|
90
|
+
any additional uniqueness checks).
|
|
91
|
+
"""
|
|
92
|
+
extra_checks = extra_check_functions or []
|
|
93
|
+
|
|
94
|
+
while True:
|
|
95
|
+
candidate = Helper.generate_unique_identifier(prefix=prefix)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
if tenant_id:
|
|
99
|
+
# For tenant-specific resource IDs, use CORE_PLATFORM_RESOURCE_ID_TABLE
|
|
100
|
+
# This table has tenant_id column for filtering
|
|
101
|
+
resource_table = getattr(db_settings, 'CORE_PLATFORM_RESOURCE_ID_TABLE', None)
|
|
102
|
+
if resource_table:
|
|
103
|
+
resource_exists = DatabaseManager.execute_scalar(
|
|
104
|
+
f"""SELECT COUNT(1) FROM {resource_table}
|
|
105
|
+
WHERE tenant_id = %s AND id = %s""",
|
|
106
|
+
(tenant_id, candidate,),
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Fallback: assume no conflict if table not configured
|
|
110
|
+
resource_exists = 0
|
|
111
|
+
else:
|
|
112
|
+
# For main/shared schema resource IDs (no tenant_id)
|
|
113
|
+
main_resource_table = getattr(db_settings, 'CORE_PLATFORM_RESOURCE_ID_TABLE', None)
|
|
114
|
+
if main_resource_table:
|
|
115
|
+
resource_exists = DatabaseManager.execute_scalar(
|
|
116
|
+
f"""SELECT COUNT(1) FROM {main_resource_table}
|
|
117
|
+
WHERE id = %s""",
|
|
118
|
+
(candidate,),
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
# Fallback: assume no conflict if table not configured
|
|
122
|
+
resource_exists = 0
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(
|
|
125
|
+
f"Failed to validate uniqueness for resource identifier {candidate}: {str(e)}",
|
|
126
|
+
exc_info=True,
|
|
127
|
+
)
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
if resource_exists and int(resource_exists or 0) > 0:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
duplicate_found = False
|
|
134
|
+
for check in extra_checks:
|
|
135
|
+
try:
|
|
136
|
+
result = check(candidate)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(
|
|
139
|
+
f"Error while executing additional uniqueness check for {candidate}: {str(e)}",
|
|
140
|
+
exc_info=True,
|
|
141
|
+
)
|
|
142
|
+
duplicate_found = True
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if result and int(result or 0) > 0:
|
|
146
|
+
duplicate_found = True
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if duplicate_found:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
return candidate
|
|
153
|
+
|
|
11
154
|
@staticmethod
|
|
12
155
|
def map_to_dto(data: list, dto_class: T) -> List[T]:
|
|
13
156
|
"""
|
|
@@ -20,7 +163,7 @@ class Helper:
|
|
|
20
163
|
"""
|
|
21
164
|
if not data:
|
|
22
165
|
return []
|
|
23
|
-
|
|
166
|
+
|
|
24
167
|
try:
|
|
25
168
|
result = []
|
|
26
169
|
for row in data:
|
|
@@ -34,3 +177,569 @@ class Helper:
|
|
|
34
177
|
except Exception as e:
|
|
35
178
|
logger.error(f"Error mapping data to DTO: {str(e)}")
|
|
36
179
|
raise
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def generate_jwt_token(data: dict, expires_delta: timedelta | None = None):
|
|
183
|
+
|
|
184
|
+
to_encode = data.copy()
|
|
185
|
+
if expires_delta:
|
|
186
|
+
expire = datetime.now(timezone.utc) + expires_delta
|
|
187
|
+
else:
|
|
188
|
+
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
|
|
189
|
+
to_encode.update({"exp": expire})
|
|
190
|
+
encoded_jwt = jwt.encode(to_encode, db_settings.SECRET_KEY, algorithm=db_settings.ALGORITHM)
|
|
191
|
+
return encoded_jwt
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def log_activity(
|
|
195
|
+
tenant_id: str,
|
|
196
|
+
action: str,
|
|
197
|
+
resource_type: str,
|
|
198
|
+
old_data: dict | None = None,
|
|
199
|
+
new_data: dict | None = None,
|
|
200
|
+
description: str | None = None,
|
|
201
|
+
user_id: Optional[str] = None,
|
|
202
|
+
):
|
|
203
|
+
"""
|
|
204
|
+
Log an activity to the activity_logs table
|
|
205
|
+
Args:
|
|
206
|
+
tenant_id: The tenant ID
|
|
207
|
+
action: The action performed (e.g., 'create', 'update', 'delete')
|
|
208
|
+
resource_type: The type of resource (e.g., 'rt-user', 'rt-group')
|
|
209
|
+
old_data: The old data before the change (optional)
|
|
210
|
+
new_data: The new data after the change (optional)
|
|
211
|
+
description: Additional description (optional)
|
|
212
|
+
user_id: The ID of the user performing the action (optional)
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def serialize_for_json(obj):
|
|
216
|
+
"""
|
|
217
|
+
Convert objects to JSON-serializable format
|
|
218
|
+
"""
|
|
219
|
+
if isinstance(obj, datetime):
|
|
220
|
+
return obj.isoformat()
|
|
221
|
+
elif isinstance(obj, date):
|
|
222
|
+
return obj.isoformat()
|
|
223
|
+
elif isinstance(obj, time):
|
|
224
|
+
return obj.isoformat()
|
|
225
|
+
elif isinstance(obj, Decimal):
|
|
226
|
+
return float(obj)
|
|
227
|
+
elif hasattr(obj, '__dict__'):
|
|
228
|
+
# Handle custom objects by converting to dict
|
|
229
|
+
return {k: serialize_for_json(v) for k, v in obj.__dict__.items()}
|
|
230
|
+
elif isinstance(obj, dict):
|
|
231
|
+
return {k: serialize_for_json(v) for k, v in obj.items()}
|
|
232
|
+
elif isinstance(obj, (list, tuple)):
|
|
233
|
+
return [serialize_for_json(item) for item in obj]
|
|
234
|
+
else:
|
|
235
|
+
return obj
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Check if table name is set
|
|
239
|
+
tenant_activity_logs_table = getattr(db_settings, 'CORE_PLATFORM_ACTIVITY_LOGS_TABLE', None)
|
|
240
|
+
if not tenant_activity_logs_table:
|
|
241
|
+
logger.error("CORE_PLATFORM_ACTIVITY_LOGS_TABLE is not configured in settings")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
log_id = Helper.generate_unique_identifier(prefix="alog")
|
|
245
|
+
time_info = Helper.current_date_time()
|
|
246
|
+
cdate = time_info["cdate"]
|
|
247
|
+
ctime = time_info["ctime"]
|
|
248
|
+
cdatetime = time_info["cdatetime"]
|
|
249
|
+
|
|
250
|
+
# Serialize data to handle datetime and other non-JSON serializable objects
|
|
251
|
+
serialized_old_data = serialize_for_json(old_data) if old_data else None
|
|
252
|
+
serialized_new_data = serialize_for_json(new_data) if new_data else None
|
|
253
|
+
|
|
254
|
+
# Convert to JSONB-compatible format
|
|
255
|
+
old_json = json.dumps(serialized_old_data) if serialized_old_data else None
|
|
256
|
+
new_json = json.dumps(serialized_new_data) if serialized_new_data else None
|
|
257
|
+
|
|
258
|
+
# Fetch user information if user_id is provided
|
|
259
|
+
performed_by_email = None
|
|
260
|
+
performed_by_contact = None
|
|
261
|
+
performed_by_fullname = None
|
|
262
|
+
|
|
263
|
+
if user_id:
|
|
264
|
+
try:
|
|
265
|
+
logger.debug(f"Fetching user information for user_id={user_id}")
|
|
266
|
+
main_users_table = getattr(db_settings, 'CORE_PLATFORM_USERS_TABLE', 'core_platform.cp_users')
|
|
267
|
+
user_data = DatabaseManager.execute_query(
|
|
268
|
+
f"""SELECT email, contact, fullname
|
|
269
|
+
FROM {main_users_table}
|
|
270
|
+
WHERE id = %s AND tenant_id = %s""",
|
|
271
|
+
(user_id, tenant_id)
|
|
272
|
+
)
|
|
273
|
+
logger.debug(f"User data query returned: {user_data}")
|
|
274
|
+
|
|
275
|
+
if user_data and len(user_data) > 0:
|
|
276
|
+
# RealDictRow result - all cursors now use RealDictCursor
|
|
277
|
+
user_record = user_data[0]
|
|
278
|
+
performed_by_email = user_record.get("email")
|
|
279
|
+
performed_by_contact = user_record.get("contact")
|
|
280
|
+
performed_by_fullname = user_record.get("fullname")
|
|
281
|
+
|
|
282
|
+
logger.debug(f"Fetched user info - email: {performed_by_email}, contact: {performed_by_contact}, fullname: {performed_by_fullname}")
|
|
283
|
+
else:
|
|
284
|
+
logger.warning(f"No user found with user_id={user_id}")
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"Failed to fetch user information for user_id={user_id}: {str(e)}", exc_info=True)
|
|
287
|
+
|
|
288
|
+
logger.info(f"Attempting to log activity: tenant_id={tenant_id}, action={action}, resource_type={resource_type}")
|
|
289
|
+
logger.debug(f"Activity log values - performed_by_email: {performed_by_email}, performed_by_contact: {performed_by_contact}, performed_by_fullname: {performed_by_fullname}")
|
|
290
|
+
|
|
291
|
+
result = DatabaseManager.execute_update(
|
|
292
|
+
f"""INSERT INTO {tenant_activity_logs_table}
|
|
293
|
+
(id, tenant_id, action, resource_type, old_data, new_data, description, performed_by_email, performed_by_contact, performed_by_fullname, cdate, ctime, cdatetime)
|
|
294
|
+
VALUES (%s, %s, %s, NULLIF(%s, '')::text, %s::jsonb, %s::jsonb, %s, %s, %s, %s, %s, %s, %s)""",
|
|
295
|
+
(log_id, tenant_id, action, resource_type, old_json, new_json, description, performed_by_email, performed_by_contact, performed_by_fullname, cdate, ctime, cdatetime),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
logger.info(f"Activity logged successfully. Rows affected: {result}")
|
|
299
|
+
|
|
300
|
+
# Verify the inserted data
|
|
301
|
+
if result > 0:
|
|
302
|
+
verify_data = DatabaseManager.execute_query(
|
|
303
|
+
f"""SELECT performed_by_email, performed_by_contact, performed_by_fullname
|
|
304
|
+
FROM {tenant_activity_logs_table}
|
|
305
|
+
WHERE tenant_id = %s AND id = %s""",
|
|
306
|
+
(tenant_id, log_id,)
|
|
307
|
+
)
|
|
308
|
+
if verify_data:
|
|
309
|
+
logger.debug(f"Verification - Inserted activity log data: {verify_data[0]}")
|
|
310
|
+
else:
|
|
311
|
+
logger.warning(f"Could not verify inserted activity log with id={log_id}")
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
# Log the error but don't fail the main operation
|
|
315
|
+
logger.error(f"Failed to log activity: {str(e)}", exc_info=True)
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def get_email_credentials(tenant_id: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
|
|
319
|
+
"""
|
|
320
|
+
Get email credentials for sending notifications.
|
|
321
|
+
If tenant_id is provided and tenant has custom email credentials, use those.
|
|
322
|
+
Otherwise, fall back to system default credentials.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
tenant_id: Optional tenant ID to check for tenant-specific credentials
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple of (email, password) or (None, None) if not configured
|
|
329
|
+
"""
|
|
330
|
+
# If tenant_id is provided, check for tenant-specific credentials
|
|
331
|
+
if tenant_id:
|
|
332
|
+
try:
|
|
333
|
+
credentials_table = getattr(db_settings, 'CORE_PLATFORM_NOTIFICATION_EMAIL_CREDENTIALS_TABLE', 'core_platform.cp_notification_email_credentials')
|
|
334
|
+
tenant_credentials = DatabaseManager.execute_query(
|
|
335
|
+
f"""SELECT notification_email, notification_password
|
|
336
|
+
FROM {credentials_table}
|
|
337
|
+
WHERE tenant_id = %s
|
|
338
|
+
AND delete_status = 'NOT_DELETED'
|
|
339
|
+
AND is_active = true""",
|
|
340
|
+
(tenant_id,),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if tenant_credentials and len(tenant_credentials) > 0:
|
|
344
|
+
tenant_email = tenant_credentials[0].get('notification_email')
|
|
345
|
+
tenant_password = tenant_credentials[0].get('notification_password')
|
|
346
|
+
|
|
347
|
+
# If tenant has both email and password configured, use them
|
|
348
|
+
if tenant_email and tenant_password:
|
|
349
|
+
logger.info(
|
|
350
|
+
f"Using tenant-specific email credentials for tenant {tenant_id}",
|
|
351
|
+
extra={
|
|
352
|
+
"extra_fields": {
|
|
353
|
+
"tenant_id": tenant_id,
|
|
354
|
+
"email": tenant_email
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
return (tenant_email, tenant_password)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.warning(
|
|
361
|
+
f"Error fetching tenant email credentials for tenant {tenant_id}: {str(e)}",
|
|
362
|
+
extra={
|
|
363
|
+
"extra_fields": {
|
|
364
|
+
"tenant_id": tenant_id,
|
|
365
|
+
"error": str(e)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Fall back to system default credentials
|
|
371
|
+
mail_sender_email = getattr(db_settings, 'MAIL_SENDER_EMAIL', None)
|
|
372
|
+
mail_sender_pwd = getattr(db_settings, 'MAIL_SENDER_PWD', None)
|
|
373
|
+
|
|
374
|
+
if tenant_id:
|
|
375
|
+
logger.info(
|
|
376
|
+
f"Using system default email credentials for tenant {tenant_id}",
|
|
377
|
+
extra={
|
|
378
|
+
"extra_fields": {
|
|
379
|
+
"tenant_id": tenant_id
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return (mail_sender_email, mail_sender_pwd)
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def send_notification(
|
|
388
|
+
email: str,
|
|
389
|
+
subject: str,
|
|
390
|
+
text_template: str,
|
|
391
|
+
html_template: str,
|
|
392
|
+
variables: Optional[Dict[str, str]] = None,
|
|
393
|
+
tenant_id: Optional[str] = None,
|
|
394
|
+
):
|
|
395
|
+
"""
|
|
396
|
+
Send a dynamic notification email.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
email: Recipient email address
|
|
400
|
+
subject: Email subject
|
|
401
|
+
text_template: Plain text email template
|
|
402
|
+
html_template: HTML email template
|
|
403
|
+
variables: Optional dictionary of variables to format templates
|
|
404
|
+
tenant_id: Optional tenant ID to use tenant-specific email credentials
|
|
405
|
+
"""
|
|
406
|
+
current_time = Helper.current_date_time()
|
|
407
|
+
cdate = current_time["cdate"]
|
|
408
|
+
ctime = current_time["ctime"]
|
|
409
|
+
|
|
410
|
+
# Add defaults if not provided
|
|
411
|
+
if variables is None:
|
|
412
|
+
variables = {}
|
|
413
|
+
|
|
414
|
+
# Include date/time automatically
|
|
415
|
+
variables.update({"cdate": cdate, "ctime": ctime})
|
|
416
|
+
|
|
417
|
+
def _escape(value):
|
|
418
|
+
if isinstance(value, str):
|
|
419
|
+
return value.replace("{", "{{").replace("}", "}}")
|
|
420
|
+
return value
|
|
421
|
+
|
|
422
|
+
safe_variables = {key: _escape(value) for key, value in variables.items()}
|
|
423
|
+
|
|
424
|
+
# Format templates dynamically using placeholders
|
|
425
|
+
text_message = text_template.format(**safe_variables)
|
|
426
|
+
html_message = html_template.format(**safe_variables)
|
|
427
|
+
|
|
428
|
+
# Get email credentials (tenant-specific or system default)
|
|
429
|
+
mail_sender_email, mail_sender_pwd = Helper.get_email_credentials(tenant_id)
|
|
430
|
+
|
|
431
|
+
if not mail_sender_email or not mail_sender_pwd:
|
|
432
|
+
logger.error(
|
|
433
|
+
"Email credentials not configured. MAIL_SENDER_EMAIL or MAIL_SENDER_PWD not set in settings",
|
|
434
|
+
extra={
|
|
435
|
+
"extra_fields": {
|
|
436
|
+
"tenant_id": tenant_id,
|
|
437
|
+
"receiver_email": email
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
notification_data = NotificationEmailServiceWriteDto(
|
|
444
|
+
sender_email=mail_sender_email,
|
|
445
|
+
receiver_email=email,
|
|
446
|
+
password=mail_sender_pwd,
|
|
447
|
+
text_message=text_message,
|
|
448
|
+
html_message=html_message,
|
|
449
|
+
subject=subject,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Send email
|
|
453
|
+
NotificationService.send_email(data=notification_data)
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def format_login_type_text(
|
|
457
|
+
login_type: str,
|
|
458
|
+
specific_days: Optional[List[str]] = None,
|
|
459
|
+
custom_start: Optional[str] = None,
|
|
460
|
+
custom_end: Optional[str] = None
|
|
461
|
+
) -> str:
|
|
462
|
+
"""
|
|
463
|
+
Format login type information into human-readable text.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
login_type: The type of login access (always_login, specific_days, custom)
|
|
467
|
+
specific_days: List of days when user can login (for specific_days type)
|
|
468
|
+
custom_start: Start datetime for custom login period
|
|
469
|
+
custom_end: End datetime for custom login period
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Formatted login type description
|
|
473
|
+
"""
|
|
474
|
+
if login_type == "always_login":
|
|
475
|
+
return "Anytime - You can login at any time"
|
|
476
|
+
elif login_type == "specific_days":
|
|
477
|
+
if specific_days:
|
|
478
|
+
days_str = ", ".join(specific_days)
|
|
479
|
+
return f"Specific Days - You can login on: {days_str}"
|
|
480
|
+
return "Specific Days"
|
|
481
|
+
elif login_type == "custom":
|
|
482
|
+
if custom_start and custom_end:
|
|
483
|
+
return f"Custom Period - From {custom_start} to {custom_end}"
|
|
484
|
+
elif custom_start:
|
|
485
|
+
return f"Custom Period - Starting from {custom_start}"
|
|
486
|
+
return "Custom Period"
|
|
487
|
+
else:
|
|
488
|
+
return "Login access granted"
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def get_users_with_admin_roles(tenant_id: str) -> List[Dict[str, str]]:
|
|
492
|
+
"""
|
|
493
|
+
Get all users with role-owner or role-admin roles in a tenant.
|
|
494
|
+
Checks both direct role assignments and group-based role assignments.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
tenant_id: The tenant ID to search within
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of dictionaries containing user_id, email, and name
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
# Get table names from settings with fallbacks
|
|
504
|
+
main_users_table = getattr(db_settings, 'CORE_PLATFORM_USERS_TABLE', 'core_platform.cp_users')
|
|
505
|
+
main_roles_table = getattr(db_settings, 'CORE_PLATFORM_ROLES_TABLE', 'core_platform.cp_roles')
|
|
506
|
+
tenant_assign_roles_table = getattr(db_settings, 'CORE_PLATFORM_ASSIGN_ROLES_TABLE', 'core_platform.cp_assign_roles')
|
|
507
|
+
tenant_user_groups_table = getattr(db_settings, 'CORE_PLATFORM_USER_GROUPS_TABLE', 'core_platform.cp_user_groups')
|
|
508
|
+
|
|
509
|
+
# Query to get users with admin roles - both direct and through groups
|
|
510
|
+
query = f"""
|
|
511
|
+
SELECT DISTINCT u.id as user_id, u.email, u.fullname
|
|
512
|
+
FROM {main_users_table} u
|
|
513
|
+
WHERE u.tenant_id = %s
|
|
514
|
+
AND u.delete_status = 'NOT_DELETED'
|
|
515
|
+
AND u.is_active = true
|
|
516
|
+
AND u.can_login = true
|
|
517
|
+
AND (
|
|
518
|
+
-- Direct role assignment
|
|
519
|
+
(u.id, u.tenant_id) IN (
|
|
520
|
+
SELECT ar.user_id, ar.tenant_id
|
|
521
|
+
FROM {tenant_assign_roles_table} ar
|
|
522
|
+
INNER JOIN {main_roles_table} r ON ar.role_id = r.id AND ar.tenant_id = r.tenant_id
|
|
523
|
+
WHERE ar.tenant_id = %s
|
|
524
|
+
AND ar.delete_status = 'NOT_DELETED'
|
|
525
|
+
AND ar.is_active = true
|
|
526
|
+
AND r.delete_status = 'NOT_DELETED'
|
|
527
|
+
AND r.is_active = true
|
|
528
|
+
AND r.role_name IN ('role-owner', 'role-admin')
|
|
529
|
+
AND ar.user_id IS NOT NULL
|
|
530
|
+
)
|
|
531
|
+
OR
|
|
532
|
+
-- Group-based role assignment
|
|
533
|
+
(u.id, u.tenant_id) IN (
|
|
534
|
+
SELECT ug.user_id, ug.tenant_id
|
|
535
|
+
FROM {tenant_user_groups_table} ug
|
|
536
|
+
INNER JOIN {tenant_assign_roles_table} ar ON ug.group_id = ar.group_id AND ug.tenant_id = ar.tenant_id
|
|
537
|
+
INNER JOIN {main_roles_table} r ON ar.role_id = r.id AND ar.tenant_id = r.tenant_id
|
|
538
|
+
WHERE ug.tenant_id = %s
|
|
539
|
+
AND ar.tenant_id = %s
|
|
540
|
+
AND ug.delete_status = 'NOT_DELETED'
|
|
541
|
+
AND ug.is_active = true
|
|
542
|
+
AND ar.delete_status = 'NOT_DELETED'
|
|
543
|
+
AND ar.is_active = true
|
|
544
|
+
AND r.delete_status = 'NOT_DELETED'
|
|
545
|
+
AND r.is_active = true
|
|
546
|
+
AND r.role_name IN ('role-owner', 'role-admin')
|
|
547
|
+
AND ar.group_id IS NOT NULL
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
results = DatabaseManager.execute_query(query, (tenant_id, tenant_id, tenant_id, tenant_id,))
|
|
553
|
+
|
|
554
|
+
admin_users = []
|
|
555
|
+
if results:
|
|
556
|
+
for row in results:
|
|
557
|
+
row_dict = dict(row)
|
|
558
|
+
admin_users.append(
|
|
559
|
+
{
|
|
560
|
+
"user_id": row_dict.get("user_id"),
|
|
561
|
+
"email": row_dict.get("email"),
|
|
562
|
+
"name": (row_dict.get("fullname") or "").strip() or "Admin",
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
logger.info(
|
|
567
|
+
f"Found {len(admin_users)} admin users for tenant {tenant_id}",
|
|
568
|
+
extra={"extra_fields": {"tenant_id": tenant_id, "admin_count": len(admin_users)}}
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return admin_users
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
logger.error(
|
|
575
|
+
f"Failed to get admin users for tenant {tenant_id}: {str(e)}",
|
|
576
|
+
extra={"extra_fields": {"tenant_id": tenant_id, "error": str(e)}},
|
|
577
|
+
exc_info=True
|
|
578
|
+
)
|
|
579
|
+
return []
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
582
|
+
def notify_admins_of_delete_status_change(
|
|
583
|
+
tenant_id: str,
|
|
584
|
+
resource_type: str,
|
|
585
|
+
resource_name: str,
|
|
586
|
+
status: str,
|
|
587
|
+
actor_user_id: Optional[str] = None,
|
|
588
|
+
message: Optional[str] = None,
|
|
589
|
+
) -> None:
|
|
590
|
+
"""
|
|
591
|
+
Notify all owner/admin users when a resource delete_status changes.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
tenant_id: Tenant identifier
|
|
595
|
+
resource_type: Human-readable resource type (e.g., "User", "Group")
|
|
596
|
+
resource_name: Human-readable resource name for context
|
|
597
|
+
status: New delete_status value ('PENDING', 'DELETED', 'NOT_DELETED')
|
|
598
|
+
actor_user_id: User ID who performed the action
|
|
599
|
+
message: Optional additional message supplied by the actor
|
|
600
|
+
"""
|
|
601
|
+
status_key = (status or "").upper()
|
|
602
|
+
status_config = {
|
|
603
|
+
"PENDING": {
|
|
604
|
+
"title": "Deletion Pending Approval",
|
|
605
|
+
"subject": "Pending deletion request for {resource_name}",
|
|
606
|
+
"description": (
|
|
607
|
+
"A deletion request has been submitted for the {resource_type} "
|
|
608
|
+
"\"{resource_name}\" and is awaiting approval."
|
|
609
|
+
),
|
|
610
|
+
"display": "Pending Deletion",
|
|
611
|
+
"color": "#ffc107",
|
|
612
|
+
"icon": "⏳",
|
|
613
|
+
},
|
|
614
|
+
"DELETED": {
|
|
615
|
+
"title": "Resource Deleted",
|
|
616
|
+
"subject": "Resource deleted: {resource_name}",
|
|
617
|
+
"description": (
|
|
618
|
+
"The {resource_type} \"{resource_name}\" has been deleted."
|
|
619
|
+
),
|
|
620
|
+
"display": "Deleted",
|
|
621
|
+
"color": "#dc3545",
|
|
622
|
+
"icon": "🗑️",
|
|
623
|
+
},
|
|
624
|
+
"NOT_DELETED": {
|
|
625
|
+
"title": "Resource Restored",
|
|
626
|
+
"subject": "Resource restored: {resource_name}",
|
|
627
|
+
"description": (
|
|
628
|
+
"The {resource_type} \"{resource_name}\" has been restored."
|
|
629
|
+
),
|
|
630
|
+
"display": "Restored",
|
|
631
|
+
"color": "#28a745",
|
|
632
|
+
"icon": "♻️",
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
config = status_config.get(status_key, status_config["DELETED"])
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
admin_users = Helper.get_users_with_admin_roles(tenant_id)
|
|
640
|
+
if not admin_users:
|
|
641
|
+
logger.info(
|
|
642
|
+
"No admin users found to notify for status change",
|
|
643
|
+
extra={
|
|
644
|
+
"extra_fields": {
|
|
645
|
+
"tenant_id": tenant_id,
|
|
646
|
+
"resource_type": resource_type,
|
|
647
|
+
"resource_name": resource_name,
|
|
648
|
+
"status": status_key,
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
actor_name = "System"
|
|
655
|
+
actor_email = "no-reply@trovesuite.com"
|
|
656
|
+
|
|
657
|
+
if actor_user_id:
|
|
658
|
+
main_users_table = getattr(db_settings, 'CORE_PLATFORM_USERS_TABLE', 'core_platform.cp_users')
|
|
659
|
+
actor_details = DatabaseManager.execute_query(
|
|
660
|
+
f"""SELECT fullname, email
|
|
661
|
+
FROM {main_users_table}
|
|
662
|
+
WHERE id = %s AND tenant_id = %s""",
|
|
663
|
+
(actor_user_id, tenant_id),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if actor_details and len(actor_details) > 0:
|
|
667
|
+
actor_data = dict(actor_details[0])
|
|
668
|
+
actor_name_candidate = (actor_data.get("fullname") or "").strip()
|
|
669
|
+
actor_name = actor_name_candidate or actor_data.get("email") or actor_name
|
|
670
|
+
actor_email = actor_data.get("email") or actor_email
|
|
671
|
+
|
|
672
|
+
resource_name_display = resource_name or "Unknown resource"
|
|
673
|
+
resource_type_display = resource_type or "Resource"
|
|
674
|
+
|
|
675
|
+
message_text = f"Message: {message}\n" if message else ""
|
|
676
|
+
message_row_style = "" if message else "display: none;"
|
|
677
|
+
message_value = message or "No additional message provided."
|
|
678
|
+
|
|
679
|
+
subject = config["subject"].format(
|
|
680
|
+
resource_name=resource_name_display,
|
|
681
|
+
resource_type=resource_type_display,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
status_description = config["description"].format(
|
|
685
|
+
resource_name=resource_name_display,
|
|
686
|
+
resource_type=resource_type_display,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
status_title = config["title"]
|
|
690
|
+
|
|
691
|
+
app_url = getattr(db_settings, 'APP_URL', 'https://app.trovesuite.com')
|
|
692
|
+
|
|
693
|
+
for admin in admin_users:
|
|
694
|
+
admin_name = admin.get("name") or "Admin"
|
|
695
|
+
Helper.send_notification(
|
|
696
|
+
email=admin["email"],
|
|
697
|
+
subject=subject,
|
|
698
|
+
text_template=RESOURCE_STATUS_CHANGE_TEXT_TEMPLATE,
|
|
699
|
+
html_template=RESOURCE_STATUS_CHANGE_HTML_TEMPLATE,
|
|
700
|
+
variables={
|
|
701
|
+
"admin_name": admin_name,
|
|
702
|
+
"resource_type": resource_type_display,
|
|
703
|
+
"resource_name": resource_name_display,
|
|
704
|
+
"status_display": config["display"],
|
|
705
|
+
"status_description": status_description,
|
|
706
|
+
"status_title": status_title,
|
|
707
|
+
"status_color": config["color"],
|
|
708
|
+
"status_icon": config["icon"],
|
|
709
|
+
"actor_name": actor_name,
|
|
710
|
+
"actor_email": actor_email,
|
|
711
|
+
"message": message_value,
|
|
712
|
+
"message_text": message_text,
|
|
713
|
+
"message_row_style": message_row_style,
|
|
714
|
+
"app_url": app_url,
|
|
715
|
+
},
|
|
716
|
+
tenant_id=tenant_id,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
logger.info(
|
|
720
|
+
"Delete status change notifications sent",
|
|
721
|
+
extra={
|
|
722
|
+
"extra_fields": {
|
|
723
|
+
"tenant_id": tenant_id,
|
|
724
|
+
"resource_type": resource_type_display,
|
|
725
|
+
"resource_name": resource_name_display,
|
|
726
|
+
"status": status_key,
|
|
727
|
+
"admin_count": len(admin_users),
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
except Exception as e:
|
|
733
|
+
logger.error(
|
|
734
|
+
f"Failed to send delete status change notifications: {str(e)}",
|
|
735
|
+
extra={
|
|
736
|
+
"extra_fields": {
|
|
737
|
+
"tenant_id": tenant_id,
|
|
738
|
+
"resource_type": resource_type,
|
|
739
|
+
"resource_name": resource_name,
|
|
740
|
+
"status": status_key,
|
|
741
|
+
"error": str(e),
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
exc_info=True,
|
|
745
|
+
)
|