trovesuite 1.0.14__py3-none-any.whl → 1.0.17__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 +5 -3
- trovesuite/auth/auth_service.py +5 -5
- trovesuite/configs/settings.py +49 -8
- trovesuite/utils/__init__.py +3 -1
- trovesuite/utils/helper.py +622 -5
- trovesuite/utils/templates.py +487 -0
- {trovesuite-1.0.14.dist-info → trovesuite-1.0.17.dist-info}/METADATA +1 -1
- {trovesuite-1.0.14.dist-info → trovesuite-1.0.17.dist-info}/RECORD +11 -10
- {trovesuite-1.0.14.dist-info → trovesuite-1.0.17.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.14.dist-info → trovesuite-1.0.17.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.14.dist-info → trovesuite-1.0.17.dist-info}/top_level.txt +0 -0
trovesuite/__init__.py
CHANGED
|
@@ -3,19 +3,21 @@ TroveSuite Package
|
|
|
3
3
|
|
|
4
4
|
A comprehensive authentication, authorization, notification, and storage service for ERP systems.
|
|
5
5
|
Provides JWT token validation, user authorization, permission checking, notification capabilities,
|
|
6
|
-
|
|
6
|
+
Azure Storage blob management, and utility functions for multi-tenant applications.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from .auth import AuthService
|
|
10
10
|
from .notification import NotificationService
|
|
11
11
|
from .storage import StorageService
|
|
12
|
+
from .utils import Helper
|
|
12
13
|
|
|
13
|
-
__version__ = "1.0.
|
|
14
|
+
__version__ = "1.0.16"
|
|
14
15
|
__author__ = "Bright Debrah Owusu"
|
|
15
16
|
__email__ = "owusu.debrah@deladetech.com"
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"AuthService",
|
|
19
20
|
"NotificationService",
|
|
20
|
-
"StorageService"
|
|
21
|
+
"StorageService",
|
|
22
|
+
"Helper",
|
|
21
23
|
]
|
trovesuite/auth/auth_service.py
CHANGED
|
@@ -229,10 +229,10 @@ class AuthService:
|
|
|
229
229
|
logger.info(f"Fetching system-level roles for user: {user_id}")
|
|
230
230
|
|
|
231
231
|
system_roles = DatabaseManager.execute_query(
|
|
232
|
-
"""
|
|
232
|
+
f"""
|
|
233
233
|
SELECT DISTINCT sug.group_id, sug.user_id, sar.role_id, sar.resource_type
|
|
234
|
-
FROM
|
|
235
|
-
INNER JOIN
|
|
234
|
+
FROM {db_settings.MAIN_SYSTEM_USER_GROUPS_TABLE} sug
|
|
235
|
+
INNER JOIN {db_settings.MAIN_SYSTEM_ASSIGN_ROLES_TABLE} sar ON sug.group_id = sar.group_id
|
|
236
236
|
WHERE sug.user_id = %s
|
|
237
237
|
AND sar.is_active = true
|
|
238
238
|
AND sar.delete_status = 'NOT_DELETED'
|
|
@@ -247,9 +247,9 @@ class AuthService:
|
|
|
247
247
|
|
|
248
248
|
# ✅ NEW: Also check for direct system role assignments (user_id in system_assign_roles)
|
|
249
249
|
direct_system_roles = DatabaseManager.execute_query(
|
|
250
|
-
"""
|
|
250
|
+
f"""
|
|
251
251
|
SELECT DISTINCT NULL as group_id, sar.user_id, sar.role_id, sar.resource_type
|
|
252
|
-
FROM
|
|
252
|
+
FROM {db_settings.MAIN_SYSTEM_ASSIGN_ROLES_TABLE} sar
|
|
253
253
|
WHERE sar.user_id = %s
|
|
254
254
|
AND sar.is_active = true
|
|
255
255
|
AND sar.delete_status = 'NOT_DELETED'
|
trovesuite/configs/settings.py
CHANGED
|
@@ -9,10 +9,10 @@ class Settings:
|
|
|
9
9
|
DB_NAME: str = os.getenv("DB_NAME")
|
|
10
10
|
DB_PORT: str = os.getenv("DB_PORT")
|
|
11
11
|
DB_PASSWORD: str = os.getenv("DB_PASSWORD")
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
# Application settings
|
|
14
|
-
APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
|
|
15
14
|
DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
|
|
15
|
+
APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
|
|
16
16
|
APP_VERSION: str = os.getenv("APP_VERSION", "1.0.0")
|
|
17
17
|
|
|
18
18
|
# Logging settings
|
|
@@ -24,26 +24,67 @@ class Settings:
|
|
|
24
24
|
LOG_DIR: str = os.getenv("LOG_DIR", "logs")
|
|
25
25
|
|
|
26
26
|
# Security settings
|
|
27
|
-
ENVIRONMENT: str = os.getenv("ENVIRONMENT")
|
|
28
27
|
ALGORITHM: str = os.getenv("ALGORITHM")
|
|
29
28
|
SECRET_KEY: str = os.getenv("SECRET_KEY")
|
|
29
|
+
ENVIRONMENT: str = os.getenv("ENVIRONMENT")
|
|
30
30
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
|
|
31
31
|
|
|
32
32
|
# =============================================================================
|
|
33
33
|
# SHARED TABLES (main schema)
|
|
34
34
|
# =============================================================================
|
|
35
|
-
MAIN_TENANTS_TABLE = os.getenv("MAIN_TENANTS_TABLE")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
MAIN_TENANTS_TABLE = os.getenv("MAIN_TENANTS_TABLE", "main.tenants")
|
|
36
|
+
MAIN_TENANT_RESOURCE_ID_TABLE = os.getenv("MAIN_TENANT_RESOURCE_ID_TABLE", "main.tenant_resource_ids")
|
|
37
|
+
MAIN_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_SUBSCRIPTIONS_TABLE", "main.subscriptions")
|
|
38
|
+
MAIN_APPS_TABLE = os.getenv("MAIN_APPS_TABLE", "main.apps")
|
|
39
|
+
MAIN_USERS_TABLE = os.getenv("MAIN_USERS_TABLE", "main.users")
|
|
40
|
+
MAIN_RESOURCE_TYPES_TABLE = os.getenv("MAIN_RESOURCE_TYPES_TABLE", "main.resource_types")
|
|
41
|
+
MAIN_RESOURCE_ID_TABLE = os.getenv("MAIN_RESOURCE_ID_TABLE", "main.resource_ids")
|
|
42
|
+
MAIN_RESOURCES_TABLE = os.getenv("MAIN_RESOURCES_TABLE", "main.resources")
|
|
43
|
+
MAIN_PERMISSIONS_TABLE = os.getenv("MAIN_PERMISSIONS_TABLE", "main.permissions")
|
|
44
|
+
MAIN_ROLES_TABLE = os.getenv("MAIN_ROLES_TABLE", "main.roles")
|
|
45
|
+
MAIN_ROLE_PERMISSIONS_TABLE = os.getenv("MAIN_ROLE_PERMISSIONS_TABLE", "main.role_permissions")
|
|
46
|
+
MAIN_USER_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_USER_SUBSCRIPTIONS_TABLE", "main.user_subscriptions")
|
|
47
|
+
MAIN_USER_SUBSCRIPTION_HISTORY_TABLE = os.getenv("MAIN_USER_SUBSCRIPTION_HISTORY_TABLE", "main.user_subscription_history")
|
|
48
|
+
MAIN_OTP = os.getenv("MAIN_OTP", "main.otps")
|
|
49
|
+
MAIN_PASSWORD_POLICY = os.getenv("MAIN_PASSWORD_POLICY", "main.password_policies")
|
|
50
|
+
MAIN_MULTI_FACTOR_SETTINGS = os.getenv("MAIN_MULTI_FACTOR_SETTINGS", "main.multi_factor_settings")
|
|
51
|
+
MAIN_USER_LOGIN_TRACKING = os.getenv("MAIN_USER_LOGIN_TRACKING", "main.user_login_tracking")
|
|
52
|
+
MAIN_ENTERPRISE_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_ENTERPRISE_SUBSCRIPTIONS_TABLE", "main.enterprise_subscriptions")
|
|
53
|
+
MAIN_CHANGE_PASSWORD_POLICY_TABLE = os.getenv("MAIN_CHANGE_PASSWORD_POLICY_TABLE", "main.change_password_policy")
|
|
40
54
|
|
|
55
|
+
# System-level tables
|
|
56
|
+
MAIN_SYSTEM_GROUPS_TABLE = os.getenv("MAIN_SYSTEM_GROUPS_TABLE", "main.system_groups")
|
|
57
|
+
MAIN_SYSTEM_USER_GROUPS_TABLE = os.getenv("MAIN_SYSTEM_USER_GROUPS_TABLE", "main.system_user_groups")
|
|
58
|
+
MAIN_SYSTEM_ASSIGN_ROLES_TABLE = os.getenv("MAIN_SYSTEM_ASSIGN_ROLES_TABLE", "main.system_assign_roles")
|
|
59
|
+
|
|
41
60
|
# =============================================================================
|
|
42
61
|
# TENANT-SPECIFIC TABLES (tenant schemas)
|
|
43
62
|
# =============================================================================
|
|
63
|
+
TENANT_SUBSCRIPTIONS_TABLE = os.getenv("TENANT_SUBSCRIPTIONS_TABLE")
|
|
64
|
+
TENANT_GROUPS_TABLE = os.getenv("TENANT_GROUPS_TABLE")
|
|
44
65
|
TENANT_LOGIN_SETTINGS_TABLE = os.getenv("TENANT_LOGIN_SETTINGS_TABLE")
|
|
66
|
+
TENANT_RESOURCES_TABLE = os.getenv("TENANT_RESOURCES_TABLE")
|
|
45
67
|
TENANT_ASSIGN_ROLES_TABLE = os.getenv("TENANT_ASSIGN_ROLES_TABLE")
|
|
68
|
+
TENANT_RESOURCE_ID_TABLE = os.getenv("TENANT_RESOURCE_ID_TABLE")
|
|
69
|
+
TENANT_SUBSCRIPTION_HISTORY_TABLE = os.getenv("TENANT_SUBSCRIPTION_HISTORY_TABLE")
|
|
70
|
+
TENANT_RESOURCE_DELETION_CHAT_HISTORY_TABLE = os.getenv("TENANT_RESOURCE_DELETION_CHAT_HISTORY_TABLE")
|
|
46
71
|
TENANT_USER_GROUPS_TABLE = os.getenv("TENANT_USER_GROUPS_TABLE")
|
|
72
|
+
TENANT_ACTIVITY_LOGS_TABLE = os.getenv("TENANT_ACTIVITY_LOGS_TABLE")
|
|
73
|
+
TENANT_ORGANIZATIONS_TABLE = os.getenv("TENANT_ORGANIZATIONS_TABLE", "organizations")
|
|
74
|
+
TENANT_BUSINESSES_TABLE = os.getenv("TENANT_BUSINESSES_TABLE", "businesses")
|
|
75
|
+
TENANT_BUSINESS_APPS_TABLE = os.getenv("TENANT_BUSINESS_APPS_TABLE", "business_apps")
|
|
76
|
+
TENANT_LOCATIONS_TABLE = os.getenv("TENANT_LOCATIONS_TABLE", "locations")
|
|
77
|
+
TENANT_ASSIGN_LOCATIONS_TABLE = os.getenv("TENANT_ASSIGN_LOCATIONS_TABLE", "assign_locations")
|
|
78
|
+
TENANT_UNIT_OF_MEASURE_TABLE = os.getenv("TENANT_UNIT_OF_MEASURE_TABLE")
|
|
79
|
+
TENANT_CURRENCY=os.getenv("TENANT_CURRENCY")
|
|
80
|
+
|
|
81
|
+
# Mail Configurations
|
|
82
|
+
MAIL_SENDER_EMAIL=os.getenv("MAIL_SENDER_EMAIL")
|
|
83
|
+
MAIL_SENDER_PWD=os.getenv("MAIL_SENDER_PWD")
|
|
84
|
+
|
|
85
|
+
# Application Configurations
|
|
86
|
+
APP_URL=os.getenv("APP_URL", "https://trovesuite.com")
|
|
87
|
+
USER_ASSIGNED_MANAGED_IDENTITY=os.getenv("USER_ASSIGNED_MANAGED_IDENTITY")
|
|
47
88
|
|
|
48
89
|
@property
|
|
49
90
|
def database_url(self) -> str:
|
trovesuite/utils/__init__.py
CHANGED
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, check tenant schema
|
|
100
|
+
# Note: TENANT_RESOURCE_ID_TABLE needs to be defined in settings
|
|
101
|
+
tenant_resource_table = getattr(db_settings, 'TENANT_RESOURCE_ID_TABLE', None)
|
|
102
|
+
if tenant_resource_table:
|
|
103
|
+
resource_exists = DatabaseManager.execute_scalar(
|
|
104
|
+
f"""SELECT COUNT(1) FROM "{tenant_id}".{tenant_resource_table}
|
|
105
|
+
WHERE id = %s""",
|
|
106
|
+
(candidate,),
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Fallback: assume no conflict if table not configured
|
|
110
|
+
resource_exists = 0
|
|
111
|
+
else:
|
|
112
|
+
# For main schema resource IDs
|
|
113
|
+
main_resource_table = getattr(db_settings, 'MAIN_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,477 @@ 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, 'TENANT_ACTIVITY_LOGS_TABLE', None)
|
|
240
|
+
if not tenant_activity_logs_table:
|
|
241
|
+
logger.error("TENANT_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, 'MAIN_USERS_TABLE', 'main.users')
|
|
267
|
+
user_data = DatabaseManager.execute_query(
|
|
268
|
+
f"""SELECT email, contact, fullname
|
|
269
|
+
FROM {main_users_table}
|
|
270
|
+
WHERE id = %s""",
|
|
271
|
+
(user_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_id}".{tenant_activity_logs_table}
|
|
293
|
+
(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, NULLIF(%s, '')::text, %s::jsonb, %s::jsonb, %s, %s, %s, %s, %s, %s, %s)""",
|
|
295
|
+
(log_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_id}".{tenant_activity_logs_table}
|
|
305
|
+
WHERE id = %s""",
|
|
306
|
+
(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 send_notification(
|
|
319
|
+
email: str,
|
|
320
|
+
subject: str,
|
|
321
|
+
text_template: str,
|
|
322
|
+
html_template: str,
|
|
323
|
+
variables: Optional[Dict[str, str]] = None,
|
|
324
|
+
):
|
|
325
|
+
"""Send a dynamic notification email."""
|
|
326
|
+
current_time = Helper.current_date_time()
|
|
327
|
+
cdate = current_time["cdate"]
|
|
328
|
+
ctime = current_time["ctime"]
|
|
329
|
+
|
|
330
|
+
# Add defaults if not provided
|
|
331
|
+
if variables is None:
|
|
332
|
+
variables = {}
|
|
333
|
+
|
|
334
|
+
# Include date/time automatically
|
|
335
|
+
variables.update({"cdate": cdate, "ctime": ctime})
|
|
336
|
+
|
|
337
|
+
def _escape(value):
|
|
338
|
+
if isinstance(value, str):
|
|
339
|
+
return value.replace("{", "{{").replace("}", "}}")
|
|
340
|
+
return value
|
|
341
|
+
|
|
342
|
+
safe_variables = {key: _escape(value) for key, value in variables.items()}
|
|
343
|
+
|
|
344
|
+
# Format templates dynamically using placeholders
|
|
345
|
+
text_message = text_template.format(**safe_variables)
|
|
346
|
+
html_message = html_template.format(**safe_variables)
|
|
347
|
+
|
|
348
|
+
# Build email data
|
|
349
|
+
mail_sender_email = getattr(db_settings, 'MAIL_SENDER_EMAIL', None)
|
|
350
|
+
mail_sender_pwd = getattr(db_settings, 'MAIL_SENDER_PWD', None)
|
|
351
|
+
|
|
352
|
+
if not mail_sender_email or not mail_sender_pwd:
|
|
353
|
+
logger.error("MAIL_SENDER_EMAIL or MAIL_SENDER_PWD not configured in settings")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
notification_data = NotificationEmailServiceWriteDto(
|
|
357
|
+
sender_email=mail_sender_email,
|
|
358
|
+
receiver_email=email,
|
|
359
|
+
password=mail_sender_pwd,
|
|
360
|
+
text_message=text_message,
|
|
361
|
+
html_message=html_message,
|
|
362
|
+
subject=subject,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Send email
|
|
366
|
+
NotificationService.send_email(data=notification_data)
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def format_login_type_text(
|
|
370
|
+
login_type: str,
|
|
371
|
+
specific_days: Optional[List[str]] = None,
|
|
372
|
+
custom_start: Optional[str] = None,
|
|
373
|
+
custom_end: Optional[str] = None
|
|
374
|
+
) -> str:
|
|
375
|
+
"""
|
|
376
|
+
Format login type information into human-readable text.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
login_type: The type of login access (always_login, specific_days, custom)
|
|
380
|
+
specific_days: List of days when user can login (for specific_days type)
|
|
381
|
+
custom_start: Start datetime for custom login period
|
|
382
|
+
custom_end: End datetime for custom login period
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Formatted login type description
|
|
386
|
+
"""
|
|
387
|
+
if login_type == "always_login":
|
|
388
|
+
return "Anytime - You can login at any time"
|
|
389
|
+
elif login_type == "specific_days":
|
|
390
|
+
if specific_days:
|
|
391
|
+
days_str = ", ".join(specific_days)
|
|
392
|
+
return f"Specific Days - You can login on: {days_str}"
|
|
393
|
+
return "Specific Days"
|
|
394
|
+
elif login_type == "custom":
|
|
395
|
+
if custom_start and custom_end:
|
|
396
|
+
return f"Custom Period - From {custom_start} to {custom_end}"
|
|
397
|
+
elif custom_start:
|
|
398
|
+
return f"Custom Period - Starting from {custom_start}"
|
|
399
|
+
return "Custom Period"
|
|
400
|
+
else:
|
|
401
|
+
return "Login access granted"
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def get_users_with_admin_roles(tenant_id: str) -> List[Dict[str, str]]:
|
|
405
|
+
"""
|
|
406
|
+
Get all users with role-owner or role-admin roles in a tenant.
|
|
407
|
+
Checks both direct role assignments and group-based role assignments.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
tenant_id: The tenant ID to search within
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
List of dictionaries containing user_id, email, and name
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
# Get table names from settings with fallbacks
|
|
417
|
+
main_users_table = getattr(db_settings, 'MAIN_USERS_TABLE', 'main.users')
|
|
418
|
+
main_roles_table = getattr(db_settings, 'MAIN_ROLES_TABLE', 'main.roles')
|
|
419
|
+
tenant_assign_roles_table = getattr(db_settings, 'TENANT_ASSIGN_ROLES_TABLE', 'assign_roles')
|
|
420
|
+
tenant_user_groups_table = getattr(db_settings, 'TENANT_USER_GROUPS_TABLE', 'user_groups')
|
|
421
|
+
|
|
422
|
+
# Query to get users with admin roles - both direct and through groups
|
|
423
|
+
query = f"""
|
|
424
|
+
SELECT DISTINCT u.id as user_id, u.email, u.fullname
|
|
425
|
+
FROM {main_users_table} u
|
|
426
|
+
WHERE u.delete_status = 'NOT_DELETED'
|
|
427
|
+
AND u.is_active = true
|
|
428
|
+
AND u.can_login = true
|
|
429
|
+
AND (
|
|
430
|
+
-- Direct role assignment
|
|
431
|
+
u.id IN (
|
|
432
|
+
SELECT ar.user_id
|
|
433
|
+
FROM "{tenant_id}".{tenant_assign_roles_table} ar
|
|
434
|
+
INNER JOIN {main_roles_table} r ON ar.role_id = r.id
|
|
435
|
+
WHERE ar.delete_status = 'NOT_DELETED'
|
|
436
|
+
AND ar.is_active = true
|
|
437
|
+
AND r.delete_status = 'NOT_DELETED'
|
|
438
|
+
AND r.is_active = true
|
|
439
|
+
AND r.role_name IN ('role-owner', 'role-admin')
|
|
440
|
+
AND ar.user_id IS NOT NULL
|
|
441
|
+
)
|
|
442
|
+
OR
|
|
443
|
+
-- Group-based role assignment
|
|
444
|
+
u.id IN (
|
|
445
|
+
SELECT ug.user_id
|
|
446
|
+
FROM "{tenant_id}".{tenant_user_groups_table} ug
|
|
447
|
+
INNER JOIN "{tenant_id}".{tenant_assign_roles_table} ar ON ug.group_id = ar.group_id
|
|
448
|
+
INNER JOIN {main_roles_table} r ON ar.role_id = r.id
|
|
449
|
+
WHERE ug.delete_status = 'NOT_DELETED'
|
|
450
|
+
AND ug.is_active = true
|
|
451
|
+
AND ar.delete_status = 'NOT_DELETED'
|
|
452
|
+
AND ar.is_active = true
|
|
453
|
+
AND r.delete_status = 'NOT_DELETED'
|
|
454
|
+
AND r.is_active = true
|
|
455
|
+
AND r.role_name IN ('role-owner', 'role-admin')
|
|
456
|
+
AND ar.group_id IS NOT NULL
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
results = DatabaseManager.execute_query(query)
|
|
462
|
+
|
|
463
|
+
admin_users = []
|
|
464
|
+
if results:
|
|
465
|
+
for row in results:
|
|
466
|
+
row_dict = dict(row)
|
|
467
|
+
admin_users.append(
|
|
468
|
+
{
|
|
469
|
+
"user_id": row_dict.get("user_id"),
|
|
470
|
+
"email": row_dict.get("email"),
|
|
471
|
+
"name": (row_dict.get("fullname") or "").strip() or "Admin",
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
logger.info(
|
|
476
|
+
f"Found {len(admin_users)} admin users for tenant {tenant_id}",
|
|
477
|
+
extra={"extra_fields": {"tenant_id": tenant_id, "admin_count": len(admin_users)}}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return admin_users
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.error(
|
|
484
|
+
f"Failed to get admin users for tenant {tenant_id}: {str(e)}",
|
|
485
|
+
extra={"extra_fields": {"tenant_id": tenant_id, "error": str(e)}},
|
|
486
|
+
exc_info=True
|
|
487
|
+
)
|
|
488
|
+
return []
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def notify_admins_of_delete_status_change(
|
|
492
|
+
tenant_id: str,
|
|
493
|
+
resource_type: str,
|
|
494
|
+
resource_name: str,
|
|
495
|
+
status: str,
|
|
496
|
+
actor_user_id: Optional[str] = None,
|
|
497
|
+
message: Optional[str] = None,
|
|
498
|
+
) -> None:
|
|
499
|
+
"""
|
|
500
|
+
Notify all owner/admin users when a resource delete_status changes.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
tenant_id: Tenant identifier
|
|
504
|
+
resource_type: Human-readable resource type (e.g., "User", "Group")
|
|
505
|
+
resource_name: Human-readable resource name for context
|
|
506
|
+
status: New delete_status value ('PENDING', 'DELETED', 'NOT_DELETED')
|
|
507
|
+
actor_user_id: User ID who performed the action
|
|
508
|
+
message: Optional additional message supplied by the actor
|
|
509
|
+
"""
|
|
510
|
+
status_key = (status or "").upper()
|
|
511
|
+
status_config = {
|
|
512
|
+
"PENDING": {
|
|
513
|
+
"title": "Deletion Pending Approval",
|
|
514
|
+
"subject": "Pending deletion request for {resource_name}",
|
|
515
|
+
"description": (
|
|
516
|
+
"A deletion request has been submitted for the {resource_type} "
|
|
517
|
+
"\"{resource_name}\" and is awaiting approval."
|
|
518
|
+
),
|
|
519
|
+
"display": "Pending Deletion",
|
|
520
|
+
"color": "#ffc107",
|
|
521
|
+
"icon": "⏳",
|
|
522
|
+
},
|
|
523
|
+
"DELETED": {
|
|
524
|
+
"title": "Resource Deleted",
|
|
525
|
+
"subject": "Resource deleted: {resource_name}",
|
|
526
|
+
"description": (
|
|
527
|
+
"The {resource_type} \"{resource_name}\" has been deleted."
|
|
528
|
+
),
|
|
529
|
+
"display": "Deleted",
|
|
530
|
+
"color": "#dc3545",
|
|
531
|
+
"icon": "🗑️",
|
|
532
|
+
},
|
|
533
|
+
"NOT_DELETED": {
|
|
534
|
+
"title": "Resource Restored",
|
|
535
|
+
"subject": "Resource restored: {resource_name}",
|
|
536
|
+
"description": (
|
|
537
|
+
"The {resource_type} \"{resource_name}\" has been restored."
|
|
538
|
+
),
|
|
539
|
+
"display": "Restored",
|
|
540
|
+
"color": "#28a745",
|
|
541
|
+
"icon": "♻️",
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
config = status_config.get(status_key, status_config["DELETED"])
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
admin_users = Helper.get_users_with_admin_roles(tenant_id)
|
|
549
|
+
if not admin_users:
|
|
550
|
+
logger.info(
|
|
551
|
+
"No admin users found to notify for status change",
|
|
552
|
+
extra={
|
|
553
|
+
"extra_fields": {
|
|
554
|
+
"tenant_id": tenant_id,
|
|
555
|
+
"resource_type": resource_type,
|
|
556
|
+
"resource_name": resource_name,
|
|
557
|
+
"status": status_key,
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
)
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
actor_name = "System"
|
|
564
|
+
actor_email = "no-reply@trovesuite.com"
|
|
565
|
+
|
|
566
|
+
if actor_user_id:
|
|
567
|
+
main_users_table = getattr(db_settings, 'MAIN_USERS_TABLE', 'main.users')
|
|
568
|
+
actor_details = DatabaseManager.execute_query(
|
|
569
|
+
f"""SELECT fullname, email
|
|
570
|
+
FROM {main_users_table}
|
|
571
|
+
WHERE id = %s""",
|
|
572
|
+
(actor_user_id,),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
if actor_details:
|
|
576
|
+
actor_data = dict(actor_details[0])
|
|
577
|
+
actor_name_candidate = (actor_data.get("fullname") or "").strip()
|
|
578
|
+
actor_name = actor_name_candidate or actor_data.get("email") or actor_name
|
|
579
|
+
actor_email = actor_data.get("email") or actor_email
|
|
580
|
+
|
|
581
|
+
resource_name_display = resource_name or "Unknown resource"
|
|
582
|
+
resource_type_display = resource_type or "Resource"
|
|
583
|
+
|
|
584
|
+
message_text = f"Message: {message}\n" if message else ""
|
|
585
|
+
message_row_style = "" if message else "display: none;"
|
|
586
|
+
message_value = message or "No additional message provided."
|
|
587
|
+
|
|
588
|
+
subject = config["subject"].format(
|
|
589
|
+
resource_name=resource_name_display,
|
|
590
|
+
resource_type=resource_type_display,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
status_description = config["description"].format(
|
|
594
|
+
resource_name=resource_name_display,
|
|
595
|
+
resource_type=resource_type_display,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
status_title = config["title"]
|
|
599
|
+
|
|
600
|
+
app_url = getattr(db_settings, 'APP_URL', 'https://app.trovesuite.com')
|
|
601
|
+
|
|
602
|
+
for admin in admin_users:
|
|
603
|
+
admin_name = admin.get("name") or "Admin"
|
|
604
|
+
Helper.send_notification(
|
|
605
|
+
email=admin["email"],
|
|
606
|
+
subject=subject,
|
|
607
|
+
text_template=RESOURCE_STATUS_CHANGE_TEXT_TEMPLATE,
|
|
608
|
+
html_template=RESOURCE_STATUS_CHANGE_HTML_TEMPLATE,
|
|
609
|
+
variables={
|
|
610
|
+
"admin_name": admin_name,
|
|
611
|
+
"resource_type": resource_type_display,
|
|
612
|
+
"resource_name": resource_name_display,
|
|
613
|
+
"status_display": config["display"],
|
|
614
|
+
"status_description": status_description,
|
|
615
|
+
"status_title": status_title,
|
|
616
|
+
"status_color": config["color"],
|
|
617
|
+
"status_icon": config["icon"],
|
|
618
|
+
"actor_name": actor_name,
|
|
619
|
+
"actor_email": actor_email,
|
|
620
|
+
"message": message_value,
|
|
621
|
+
"message_text": message_text,
|
|
622
|
+
"message_row_style": message_row_style,
|
|
623
|
+
"app_url": app_url,
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
logger.info(
|
|
628
|
+
"Delete status change notifications sent",
|
|
629
|
+
extra={
|
|
630
|
+
"extra_fields": {
|
|
631
|
+
"tenant_id": tenant_id,
|
|
632
|
+
"resource_type": resource_type_display,
|
|
633
|
+
"resource_name": resource_name_display,
|
|
634
|
+
"status": status_key,
|
|
635
|
+
"admin_count": len(admin_users),
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.error(
|
|
642
|
+
f"Failed to send delete status change notifications: {str(e)}",
|
|
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
|
+
"error": str(e),
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
exc_info=True,
|
|
653
|
+
)
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
OTP_TEXT_TEMPLATE = (
|
|
2
|
+
"Your Trovesuite OTP code is: {otp_code}\n"
|
|
3
|
+
"Please do not share this code with anyone.\n"
|
|
4
|
+
"This code will expire in 5 minutes.\n"
|
|
5
|
+
"Sent at: {cdate}, {ctime}"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
OTP_HTML_TEMPLATE = """
|
|
9
|
+
<html>
|
|
10
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;">
|
|
11
|
+
<div style="max-width: 500px; margin: auto; background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 6px rgba(0,0,0,0.1);">
|
|
12
|
+
<h2 style="color: #2e86de; text-align: center;">🔐 {message_header}</h2>
|
|
13
|
+
<p style="font-size: 16px; color: #333;">Dear {user_name},</p>
|
|
14
|
+
<p style="font-size: 15px; color: #333;">
|
|
15
|
+
Please use the following <b>One-Time Password (OTP)</b> to complete your verification:
|
|
16
|
+
</p>
|
|
17
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
18
|
+
<div style="display: inline-block; background-color: #2e86de; color: white; padding: 15px 30px; border-radius: 8px; font-size: 28px; letter-spacing: 4px;">
|
|
19
|
+
{otp_code}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<p style="font-size: 14px; color: #555;">
|
|
23
|
+
This code will expire in <b>5 minutes</b>. Please do not share this code with anyone.
|
|
24
|
+
</p>
|
|
25
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
26
|
+
<p style="font-size: 12px; color: #888; text-align: center;">
|
|
27
|
+
Trovesuite Security System<br>
|
|
28
|
+
Sent at: {cdate}, {ctime}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
PASSWORD_CHANGE_HTML_TEMPLATE = """
|
|
36
|
+
<html>
|
|
37
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;">
|
|
38
|
+
<div style="max-width: 500px; margin: auto; background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 6px rgba(0,0,0,0.1);">
|
|
39
|
+
<h2 style="color: #2e86de; text-align: center;">🔒 Password Change Alert</h2>
|
|
40
|
+
<p style="font-size: 16px; color: #333;">Hello {user_name},</p>
|
|
41
|
+
<p style="font-size: 15px; color: #333;">
|
|
42
|
+
We noticed that the password for your <b>Trovesuite</b> account was recently changed.
|
|
43
|
+
</p>
|
|
44
|
+
<p style="font-size: 15px; color: #333;">
|
|
45
|
+
If you made this change, you can safely ignore this message.
|
|
46
|
+
</p>
|
|
47
|
+
<p style="font-size: 15px; color: #333;">
|
|
48
|
+
However, if you did <b>not</b> make this change, please take immediate action to secure your account.
|
|
49
|
+
</p>
|
|
50
|
+
<div style="margin-top: 30px; padding: 15px; background-color: #f1f3f6; border-radius: 8px;">
|
|
51
|
+
<p style="font-size: 13px; color: #555; margin: 0;">
|
|
52
|
+
Date: <b>{cdate}</b><br>
|
|
53
|
+
Time: <b>{ctime}</b>
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
57
|
+
<p style="font-size: 12px; color: #888; text-align: center;">
|
|
58
|
+
Trovesuite Security System<br>
|
|
59
|
+
Ensuring your data safety always.<br>
|
|
60
|
+
Sent at: {cdate}, {ctime}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
PASSWORD_CHANGE_TEXT_TEMPLATE = (
|
|
68
|
+
"Hello {user_name},\n\n"
|
|
69
|
+
"We wanted to let you know that the password for your Trovesuite account was just changed.\n\n"
|
|
70
|
+
"If you made this change, you can safely ignore this message.\n"
|
|
71
|
+
"If you did NOT make this change, please take immediate action to secure your account.\n\n"
|
|
72
|
+
"Date: {cdate}\n"
|
|
73
|
+
"Time: {ctime}\n"
|
|
74
|
+
"— The Trovesuite Security Team"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
ACCESS_GRANTED_HTML_TEMPLATE = """
|
|
78
|
+
<html>
|
|
79
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px; margin: 0;">
|
|
80
|
+
<div style="max-width: 600px; margin: auto; background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
|
81
|
+
<!-- Header -->
|
|
82
|
+
<div style="text-align: center; padding-bottom: 25px; border-bottom: 2px solid #2e86de;">
|
|
83
|
+
<h1 style="color: #2e86de; margin: 0; font-size: 28px;">🎉 Welcome to Trovesuite!</h1>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Greeting -->
|
|
87
|
+
<div style="padding: 25px 0;">
|
|
88
|
+
<p style="font-size: 16px; color: #333; margin-bottom: 15px;">Dear <strong>{user_name}</strong>,</p>
|
|
89
|
+
<p style="font-size: 15px; color: #333; line-height: 1.6;">
|
|
90
|
+
Great news! You have been granted access to <strong>Trovesuite</strong>.
|
|
91
|
+
You can now log in and start exploring all the features available to you.
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Login Details -->
|
|
96
|
+
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
|
97
|
+
<h3 style="color: #2e86de; margin-top: 0; font-size: 18px; margin-bottom: 15px;">📋 Your Login Details</h3>
|
|
98
|
+
|
|
99
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
100
|
+
<tr>
|
|
101
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; width: 40%;">
|
|
102
|
+
<strong>App URL:</strong>
|
|
103
|
+
</td>
|
|
104
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
105
|
+
<a href="{app_url}" style="color: #2e86de; text-decoration: none; font-weight: 500;">{app_url}</a>
|
|
106
|
+
</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr>
|
|
109
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
110
|
+
<strong>Email:</strong>
|
|
111
|
+
</td>
|
|
112
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
113
|
+
{user_email}
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<tr>
|
|
117
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
118
|
+
<strong>Password:</strong>
|
|
119
|
+
</td>
|
|
120
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px; font-family: 'Courier New', monospace; background-color: #fff; padding: 5px 10px; border-radius: 4px; display: inline-block;">
|
|
121
|
+
{password}
|
|
122
|
+
</td>
|
|
123
|
+
</tr>
|
|
124
|
+
<tr>
|
|
125
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
126
|
+
<strong>Login Access:</strong>
|
|
127
|
+
</td>
|
|
128
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
129
|
+
{login_type_text}
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
</table>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- CTA Button -->
|
|
136
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
137
|
+
<a href="{app_url}" style="display: inline-block; background-color: #2e86de; color: white; padding: 14px 35px; text-decoration: none; border-radius: 8px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(46, 134, 222, 0.3);">
|
|
138
|
+
Login Now →
|
|
139
|
+
</a>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Security Note -->
|
|
143
|
+
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 25px 0; border-radius: 4px;">
|
|
144
|
+
<p style="font-size: 13px; color: #856404; margin: 0; line-height: 1.5;">
|
|
145
|
+
<strong>⚠️ Security Reminder:</strong> For your security, we recommend changing your password upon your first login.
|
|
146
|
+
Please do not share your credentials with anyone.
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Footer -->
|
|
151
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
152
|
+
<div style="text-align: center;">
|
|
153
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
154
|
+
Trovesuite - Empowering Your Business<br>
|
|
155
|
+
Sent on: {cdate} at {ctime}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
ACCESS_GRANTED_TEXT_TEMPLATE = (
|
|
164
|
+
"Welcome to Trovesuite!\n\n"
|
|
165
|
+
"Dear {user_name},\n\n"
|
|
166
|
+
"Great news! You have been granted access to Trovesuite.\n"
|
|
167
|
+
"You can now log in and start exploring all the features available to you.\n\n"
|
|
168
|
+
"YOUR LOGIN DETAILS:\n"
|
|
169
|
+
"==================\n"
|
|
170
|
+
"App URL: {app_url}\n"
|
|
171
|
+
"Email: {user_email}\n"
|
|
172
|
+
"Password: {password}\n"
|
|
173
|
+
"Login Access: {login_type_text}\n\n"
|
|
174
|
+
"SECURITY REMINDER:\n"
|
|
175
|
+
"For your security, we recommend changing your password upon your first login.\n"
|
|
176
|
+
"Please do not share your credentials with anyone.\n\n"
|
|
177
|
+
"— The Trovesuite Team\n"
|
|
178
|
+
"Sent on: {cdate} at {ctime}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
RESET_PASSWORD_TEXT_TEMPLATE = (
|
|
182
|
+
"Trovesuite Password Reset\n\n"
|
|
183
|
+
"Dear {user_name},\n\n"
|
|
184
|
+
"Your Trovesuite account password has been reset by an administrator.\n\n"
|
|
185
|
+
"NEW LOGIN DETAILS:\n"
|
|
186
|
+
"==================\n"
|
|
187
|
+
"App URL: {app_url}\n"
|
|
188
|
+
"Email: {user_email}\n"
|
|
189
|
+
"Temporary Password: {password}\n\n"
|
|
190
|
+
"SECURITY REMINDER:\n"
|
|
191
|
+
"Please sign in and change this temporary password immediately. "
|
|
192
|
+
"Do not share your credentials with anyone.\n\n"
|
|
193
|
+
"— The Trovesuite Team\n"
|
|
194
|
+
"Sent on: {cdate} at {ctime}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
RESET_PASSWORD_HTML_TEMPLATE = """
|
|
198
|
+
<html>
|
|
199
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px; margin: 0;">
|
|
200
|
+
<div style="max-width: 600px; margin: auto; background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
|
201
|
+
<!-- Header -->
|
|
202
|
+
<div style="text-align: center; padding-bottom: 25px; border-bottom: 2px solid #2e86de;">
|
|
203
|
+
<h1 style="color: #2e86de; margin: 0; font-size: 26px;">🔑 Password Reset</h1>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- Greeting -->
|
|
207
|
+
<div style="padding: 25px 0;">
|
|
208
|
+
<p style="font-size: 16px; color: #333; margin-bottom: 15px;">Dear <strong>{user_name}</strong>,</p>
|
|
209
|
+
<p style="font-size: 15px; color: #333; line-height: 1.6;">
|
|
210
|
+
Your Trovesuite account password has been reset by an administrator. Use the credentials below to sign in.
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- Login Details -->
|
|
215
|
+
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
|
216
|
+
<h3 style="color: #2e86de; margin-top: 0; font-size: 18px; margin-bottom: 15px;">📋 New Login Details</h3>
|
|
217
|
+
|
|
218
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
219
|
+
<tr>
|
|
220
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; width: 35%;">
|
|
221
|
+
<strong>App URL:</strong>
|
|
222
|
+
</td>
|
|
223
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
224
|
+
<a href="{app_url}" style="color: #2e86de; text-decoration: none; font-weight: 500;">{app_url}</a>
|
|
225
|
+
</td>
|
|
226
|
+
</tr>
|
|
227
|
+
<tr>
|
|
228
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
229
|
+
<strong>Email:</strong>
|
|
230
|
+
</td>
|
|
231
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
232
|
+
{user_email}
|
|
233
|
+
</td>
|
|
234
|
+
</tr>
|
|
235
|
+
<tr>
|
|
236
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
237
|
+
<strong>Temporary Password:</strong>
|
|
238
|
+
</td>
|
|
239
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px; font-family: 'Courier New', monospace; background-color: #fff; padding: 5px 10px; border-radius: 4px; display: inline-block;">
|
|
240
|
+
{password}
|
|
241
|
+
</td>
|
|
242
|
+
</tr>
|
|
243
|
+
</table>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- Security Note -->
|
|
247
|
+
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 25px 0; border-radius: 4px;">
|
|
248
|
+
<p style="font-size: 13px; color: #856404; margin: 0; line-height: 1.5;">
|
|
249
|
+
<strong>⚠️ Important:</strong> For security reasons, please change this temporary password immediately after logging in.
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Footer -->
|
|
254
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
255
|
+
<div style="text-align: center;">
|
|
256
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
257
|
+
This is an automated notification from Trovesuite
|
|
258
|
+
</p>
|
|
259
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
260
|
+
Trovesuite - Empowering Your Business<br>
|
|
261
|
+
Sent on: {cdate} at {ctime}
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</body>
|
|
266
|
+
</html>
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
RESOURCE_DELETION_HTML_TEMPLATE = """
|
|
270
|
+
<html>
|
|
271
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px; margin: 0;">
|
|
272
|
+
<div style="max-width: 600px; margin: auto; background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
|
273
|
+
<!-- Header -->
|
|
274
|
+
<div style="text-align: center; padding-bottom: 25px; border-bottom: 2px solid #dc3545;">
|
|
275
|
+
<h1 style="color: #dc3545; margin: 0; font-size: 28px;">🗑️ Resource Deletion Notice</h1>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Greeting -->
|
|
279
|
+
<div style="padding: 25px 0;">
|
|
280
|
+
<p style="font-size: 16px; color: #333; margin-bottom: 15px;">Dear <strong>{admin_name}</strong>,</p>
|
|
281
|
+
<p style="font-size: 15px; color: #333; line-height: 1.6;">
|
|
282
|
+
This is to notify you that a resource has been deleted in your Trovesuite account.
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- Deletion Details -->
|
|
287
|
+
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 20px; margin: 20px 0; border-radius: 4px;">
|
|
288
|
+
<h3 style="color: #856404; margin-top: 0; font-size: 18px; margin-bottom: 15px;">📋 Deletion Details</h3>
|
|
289
|
+
|
|
290
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
291
|
+
<tr>
|
|
292
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; width: 35%;">
|
|
293
|
+
<strong>Resource Type:</strong>
|
|
294
|
+
</td>
|
|
295
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
296
|
+
{resource_type}
|
|
297
|
+
</td>
|
|
298
|
+
</tr>
|
|
299
|
+
<tr>
|
|
300
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
301
|
+
<strong>Resource Name:</strong>
|
|
302
|
+
</td>
|
|
303
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
304
|
+
{resource_name}
|
|
305
|
+
</td>
|
|
306
|
+
</tr>
|
|
307
|
+
<tr>
|
|
308
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
309
|
+
<strong>Deleted By:</strong>
|
|
310
|
+
</td>
|
|
311
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
312
|
+
{deleted_by_name} ({deleted_by_email})
|
|
313
|
+
</td>
|
|
314
|
+
</tr>
|
|
315
|
+
<tr>
|
|
316
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
317
|
+
<strong>Deletion Time:</strong>
|
|
318
|
+
</td>
|
|
319
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
320
|
+
{cdate} at {ctime}
|
|
321
|
+
</td>
|
|
322
|
+
</tr>
|
|
323
|
+
<tr style="{message_row_style}">
|
|
324
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; vertical-align: top;">
|
|
325
|
+
<strong>Message:</strong>
|
|
326
|
+
</td>
|
|
327
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
328
|
+
{message}
|
|
329
|
+
</td>
|
|
330
|
+
</tr>
|
|
331
|
+
</table>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<!-- Action Note -->
|
|
335
|
+
<div style="background-color: #f8f9fa; padding: 15px; margin: 25px 0; border-radius: 4px; border-left: 4px solid #2e86de;">
|
|
336
|
+
<p style="font-size: 13px; color: #333; margin: 0; line-height: 1.5;">
|
|
337
|
+
<strong>ℹ️ Note:</strong> This resource has been soft-deleted and can be restored if needed.
|
|
338
|
+
Please contact the person who deleted it or log in to your account to review this action.
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- CTA Button -->
|
|
343
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
344
|
+
<a href="{app_url}" style="display: inline-block; background-color: #2e86de; color: white; padding: 14px 35px; text-decoration: none; border-radius: 8px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(46, 134, 222, 0.3);">
|
|
345
|
+
Login to Review →
|
|
346
|
+
</a>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<!-- Footer -->
|
|
350
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
351
|
+
<div style="text-align: center;">
|
|
352
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
353
|
+
This is an automated notification from Trovesuite
|
|
354
|
+
</p>
|
|
355
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
356
|
+
Trovesuite - Empowering Your Business<br>
|
|
357
|
+
Sent on: {cdate} at {ctime}
|
|
358
|
+
</p>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</body>
|
|
362
|
+
</html>
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
RESOURCE_DELETION_TEXT_TEMPLATE = (
|
|
366
|
+
"Resource Deletion Notice\n\n"
|
|
367
|
+
"Dear {admin_name},\n\n"
|
|
368
|
+
"This is to notify you that a resource has been deleted in your Trovesuite account.\n\n"
|
|
369
|
+
"DELETION DETAILS:\n"
|
|
370
|
+
"==================\n"
|
|
371
|
+
"Resource Type: {resource_type}\n"
|
|
372
|
+
"Resource Name: {resource_name}\n"
|
|
373
|
+
"Deleted By: {deleted_by_name} ({deleted_by_email})\n"
|
|
374
|
+
"Deletion Time: {cdate} at {ctime}\n"
|
|
375
|
+
"{message_text}"
|
|
376
|
+
"\n"
|
|
377
|
+
"NOTE:\n"
|
|
378
|
+
"This resource has been soft-deleted and can be restored if needed.\n"
|
|
379
|
+
"Please contact the person who deleted it or log in to your account to review this action.\n\n"
|
|
380
|
+
"Login: {app_url}\n\n"
|
|
381
|
+
"— The Trovesuite Team\n"
|
|
382
|
+
"Sent on: {cdate} at {ctime}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
RESOURCE_STATUS_CHANGE_HTML_TEMPLATE = """
|
|
386
|
+
<html>
|
|
387
|
+
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px; margin: 0;">
|
|
388
|
+
<div style="max-width: 600px; margin: auto; background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
|
389
|
+
<!-- Header -->
|
|
390
|
+
<div style="text-align: center; padding-bottom: 25px; border-bottom: 2px solid {status_color};">
|
|
391
|
+
<h1 style="color: {status_color}; margin: 0; font-size: 28px;">{status_icon} {status_title}</h1>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- Greeting -->
|
|
395
|
+
<div style="padding: 25px 0;">
|
|
396
|
+
<p style="font-size: 16px; color: #333; margin-bottom: 15px;">Dear <strong>{admin_name}</strong>,</p>
|
|
397
|
+
<p style="font-size: 15px; color: #333; line-height: 1.6;">
|
|
398
|
+
{status_description}
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<!-- Status Details -->
|
|
403
|
+
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
|
404
|
+
<h3 style="color: {status_color}; margin-top: 0; font-size: 18px; margin-bottom: 15px;">📋 Status Details</h3>
|
|
405
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
406
|
+
<tr>
|
|
407
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; width: 35%;">
|
|
408
|
+
<strong>Resource Type:</strong>
|
|
409
|
+
</td>
|
|
410
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
411
|
+
{resource_type}
|
|
412
|
+
</td>
|
|
413
|
+
</tr>
|
|
414
|
+
<tr>
|
|
415
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
416
|
+
<strong>Resource Name:</strong>
|
|
417
|
+
</td>
|
|
418
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
419
|
+
{resource_name}
|
|
420
|
+
</td>
|
|
421
|
+
</tr>
|
|
422
|
+
<tr>
|
|
423
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
424
|
+
<strong>Status:</strong>
|
|
425
|
+
</td>
|
|
426
|
+
<td style="padding: 8px 0; color: {status_color}; font-size: 14px; font-weight: 600;">
|
|
427
|
+
{status_display}
|
|
428
|
+
</td>
|
|
429
|
+
</tr>
|
|
430
|
+
<tr>
|
|
431
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px;">
|
|
432
|
+
<strong>Triggered By:</strong>
|
|
433
|
+
</td>
|
|
434
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
435
|
+
{actor_name} ({actor_email})
|
|
436
|
+
</td>
|
|
437
|
+
</tr>
|
|
438
|
+
<tr style="{message_row_style}">
|
|
439
|
+
<td style="padding: 8px 0; color: #666; font-size: 14px; vertical-align: top;">
|
|
440
|
+
<strong>Message:</strong>
|
|
441
|
+
</td>
|
|
442
|
+
<td style="padding: 8px 0; color: #333; font-size: 14px;">
|
|
443
|
+
{message}
|
|
444
|
+
</td>
|
|
445
|
+
</tr>
|
|
446
|
+
</table>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<!-- CTA Button -->
|
|
450
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
451
|
+
<a href="{app_url}" style="display: inline-block; background-color: #2e86de; color: white; padding: 14px 35px; text-decoration: none; border-radius: 8px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(46, 134, 222, 0.3);">
|
|
452
|
+
Login to Review →
|
|
453
|
+
</a>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<!-- Footer -->
|
|
457
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
|
|
458
|
+
<div style="text-align: center;">
|
|
459
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
460
|
+
This is an automated notification from Trovesuite
|
|
461
|
+
</p>
|
|
462
|
+
<p style="font-size: 12px; color: #888; margin: 5px 0;">
|
|
463
|
+
Trovesuite - Empowering Your Business<br>
|
|
464
|
+
Sent on: {cdate} at {ctime}
|
|
465
|
+
</p>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</body>
|
|
469
|
+
</html>
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
RESOURCE_STATUS_CHANGE_TEXT_TEMPLATE = (
|
|
473
|
+
"Resource Status Update\n\n"
|
|
474
|
+
"Dear {admin_name},\n\n"
|
|
475
|
+
"{status_description}\n\n"
|
|
476
|
+
"DETAILS:\n"
|
|
477
|
+
"========\n"
|
|
478
|
+
"Resource Type: {resource_type}\n"
|
|
479
|
+
"Resource Name: {resource_name}\n"
|
|
480
|
+
"Status: {status_display}\n"
|
|
481
|
+
"Triggered By: {actor_name} ({actor_email})\n"
|
|
482
|
+
"{message_text}"
|
|
483
|
+
"\n"
|
|
484
|
+
"Login: {app_url}\n\n"
|
|
485
|
+
"— The Trovesuite Team\n"
|
|
486
|
+
"Sent on: {cdate} at {ctime}"
|
|
487
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trovesuite
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
4
4
|
Summary: TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications
|
|
5
5
|
Home-page: https://dev.azure.com/brightgclt/trovesuite/_git/packages
|
|
6
6
|
Author: Bright Debrah Owusu
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
trovesuite/__init__.py,sha256=
|
|
1
|
+
trovesuite/__init__.py,sha256=_L_7xg3oeOXLmq1MpXPwZvkXiPL0zrw-H3STHUmEMNg,646
|
|
2
2
|
trovesuite/auth/__init__.py,sha256=OjZllVvjul1glDazJ-d5TrNjgHFigFlQQi1G99DYshk,239
|
|
3
3
|
trovesuite/auth/auth_base.py,sha256=rZHQVLeJRBQ8GClgF5UwG-er4_HXVX5-nt8o6_Z29uY,75
|
|
4
4
|
trovesuite/auth/auth_controller.py,sha256=PAgaVlf5TYEfkSfK4vGGsvO84i8zEmeVVXyUF2YBppI,420
|
|
5
5
|
trovesuite/auth/auth_read_dto.py,sha256=e27JqKVPVUM83A_mYF452QCflsvGNo7aKje7q_urwFc,571
|
|
6
|
-
trovesuite/auth/auth_service.py,sha256=
|
|
6
|
+
trovesuite/auth/auth_service.py,sha256=G2_lkXdMnuqpBjKFYGlRUrUvVyq8keukXaSyboiWiUM,18106
|
|
7
7
|
trovesuite/auth/auth_write_dto.py,sha256=rdwI7w6-9QZGv1H0PAGrjkLBCzaMHjgPIXeLb9RmNec,234
|
|
8
8
|
trovesuite/configs/__init__.py,sha256=h1mSZOaZ3kUy1ZMO_m9O9KklsxywM0RfMVZLh9h9WvQ,328
|
|
9
9
|
trovesuite/configs/database.py,sha256=IPSu8fXjxyYeJ3bFknJG06Qm2L2ub6Ht19xhKv8g7nA,11731
|
|
10
10
|
trovesuite/configs/logging.py,sha256=mGjR2d4urVNry9l5_aXycMMtcY2RAFIpEL35hw33KZg,9308
|
|
11
|
-
trovesuite/configs/settings.py,sha256=
|
|
11
|
+
trovesuite/configs/settings.py,sha256=9kl4f7AOtulUnmoWcrLVa8LKjGirZkPYSWtACoTqr5U,5652
|
|
12
12
|
trovesuite/entities/__init__.py,sha256=Dbl_03Bueyh2vOP2hykd40MmNMrl5nNHSRGP-kqwwNo,160
|
|
13
13
|
trovesuite/entities/health.py,sha256=KaW7yxTQdymIPlnkJJkDqEebBXkD0a7A66i5GgNZLoE,2700
|
|
14
14
|
trovesuite/entities/sh_response.py,sha256=1_sw3PpVaDxWsNiBU0W9YLHZgTFxEj4JJBLBfSY63Ho,1579
|
|
@@ -24,10 +24,11 @@ trovesuite/storage/storage_controller.py,sha256=Yzki0L-jmSPbiw8spFm6Z84Bq-WZfih_
|
|
|
24
24
|
trovesuite/storage/storage_read_dto.py,sha256=o7EVJdwrwVZAaeyGU9O01WMECGVaytkvLRwruA256hQ,1471
|
|
25
25
|
trovesuite/storage/storage_service.py,sha256=V7LIePIV6b_iuhm-9x8r4zwpZHgeRPL1YIe5IBnxhco,19768
|
|
26
26
|
trovesuite/storage/storage_write_dto.py,sha256=vl1iCZ93bpFmpvkCrn587QtMtOA_TPDseXSoTuj9RTQ,1355
|
|
27
|
-
trovesuite/utils/__init__.py,sha256=
|
|
28
|
-
trovesuite/utils/helper.py,sha256=
|
|
29
|
-
trovesuite
|
|
30
|
-
trovesuite-1.0.
|
|
31
|
-
trovesuite-1.0.
|
|
32
|
-
trovesuite-1.0.
|
|
33
|
-
trovesuite-1.0.
|
|
27
|
+
trovesuite/utils/__init__.py,sha256=mDZuY77BphvQFYLmcWxjP5Tcq9ZZ3WXJWBKB1v6wzHU,185
|
|
28
|
+
trovesuite/utils/helper.py,sha256=k7Meg8sN4aPBdqcjaF1tQcZ32KvuA-IqjpWniQP_Mpk,26438
|
|
29
|
+
trovesuite/utils/templates.py,sha256=_92k4-EkqWs-h0LNJxPgorbspmp24kDngS7O3qWIFyQ,20388
|
|
30
|
+
trovesuite-1.0.17.dist-info/licenses/LICENSE,sha256=EJT35ct-Q794JYPdAQy3XNczQGKkU1HzToLeK1YVw2s,1070
|
|
31
|
+
trovesuite-1.0.17.dist-info/METADATA,sha256=UB2VoJbb7XXl1GUPVjq8BL_zXB_DMQMzSmuYabHBuAo,21737
|
|
32
|
+
trovesuite-1.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
trovesuite-1.0.17.dist-info/top_level.txt,sha256=GzKhG_-MTaxeHrIgkGkBH_nof2vroGFBrjeHKWUIwNc,11
|
|
34
|
+
trovesuite-1.0.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|