django-cfg 1.1.81__py3-none-any.whl → 1.2.0__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.
Files changed (246) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/templates/guide.md +266 -0
  198. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  199. django_cfg/modules/django_unfold/__init__.py +69 -0
  200. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  201. django_cfg/modules/django_unfold/dashboard.py +278 -0
  202. django_cfg/modules/django_unfold/icons/README.md +145 -0
  203. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  204. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  205. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  206. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  207. django_cfg/modules/django_unfold/models/config.py +601 -0
  208. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  209. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  210. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  211. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  212. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  213. django_cfg/modules/django_unfold/utils.py +140 -0
  214. django_cfg/registry/__init__.py +23 -0
  215. django_cfg/registry/core.py +61 -0
  216. django_cfg/registry/exceptions.py +11 -0
  217. django_cfg/registry/modules.py +12 -0
  218. django_cfg/registry/services.py +26 -0
  219. django_cfg/registry/third_party.py +52 -0
  220. django_cfg/routing/__init__.py +19 -0
  221. django_cfg/routing/callbacks.py +198 -0
  222. django_cfg/routing/routers.py +48 -0
  223. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  224. django_cfg/templatetags/__init__.py +0 -0
  225. django_cfg/templatetags/django_cfg.py +33 -0
  226. django_cfg/urls.py +33 -0
  227. django_cfg/utils/path_resolution.py +1 -1
  228. django_cfg/utils/smart_defaults.py +7 -61
  229. django_cfg/utils/toolkit.py +663 -0
  230. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
  231. django_cfg-1.2.0.dist-info/RECORD +441 -0
  232. django_cfg/apps/tasks/@docs/README.md +0 -195
  233. django_cfg/archive/django_sample.zip +0 -0
  234. django_cfg/models/unfold.py +0 -271
  235. django_cfg/modules/unfold/__init__.py +0 -29
  236. django_cfg/modules/unfold/dashboard.py +0 -318
  237. django_cfg/pyproject.toml +0 -370
  238. django_cfg/routers.py +0 -83
  239. django_cfg-1.1.81.dist-info/RECORD +0 -278
  240. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  241. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  242. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  243. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  244. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,42 +2,18 @@ import logging
2
2
  import traceback
3
3
  from django.utils import timezone
4
4
  from django.db import transaction
5
- from typing import Optional, Tuple
6
- import re
5
+ from typing import Optional
7
6
 
8
7
  from django_cfg.modules.django_telegram import DjangoTelegram
9
- from django_cfg.modules.django_twilio import SimpleTwilioService
10
8
  from ..models import OTPSecret, CustomUser
11
- from ..utils.notifications import AccountNotifications
9
+ from ..utils.auth_email_service import AuthEmailService
12
10
  from ..signals import notify_failed_otp_attempt
13
11
 
14
12
  logger = logging.getLogger(__name__)
15
13
 
16
14
 
17
15
  class OTPService:
18
- """Simple OTP service for authentication supporting both email and phone channels."""
19
-
20
- @staticmethod
21
- def _validate_phone(phone: str) -> bool:
22
- """Validate phone number format."""
23
- if not phone:
24
- return False
25
- # Clean phone number - remove spaces, dashes, parentheses
26
- clean_phone = phone.replace(' ', '').replace('-', '').replace('(', '').replace(')', '')
27
- # Basic phone validation - E.164 format: +[1-9][0-9]{6,14}
28
- phone_pattern = r'^\+[1-9]\d{6,14}$' # E.164 format: minimum 7 digits, maximum 15
29
- return bool(re.match(phone_pattern, clean_phone))
30
-
31
- @staticmethod
32
- def _determine_channel(identifier: str) -> str:
33
- """Determine if identifier is email or phone."""
34
- if '@' in identifier:
35
- return 'email'
36
- elif identifier.startswith('+') or identifier.replace(' ', '').replace('-', '').replace('(', '').replace(')', '').isdigit():
37
- return 'phone'
38
- else:
39
- # Default to email for backward compatibility
40
- return 'email'
16
+ """Simple OTP service for authentication."""
41
17
 
42
18
  @staticmethod
43
19
  def _get_otp_url(otp_code: str) -> str:
@@ -56,212 +32,132 @@ class OTPService:
56
32
 
57
33
  @staticmethod
58
34
  @transaction.atomic
59
- def request_otp(
60
- identifier: str,
61
- channel: Optional[str] = None,
62
- source_url: Optional[str] = None
63
- ) -> Tuple[bool, str]:
64
- """Generate and send OTP to email or phone. Returns (success, error_type)."""
65
- return OTPService._request_otp_internal(identifier, channel, source_url)
66
-
67
- @staticmethod
68
- @transaction.atomic
69
- def request_email_otp(email: str, source_url: Optional[str] = None) -> Tuple[bool, str]:
70
- """Generate and send OTP to email (backward compatibility)."""
71
- return OTPService._request_otp_internal(email, 'email', source_url)
72
-
73
- @staticmethod
74
- @transaction.atomic
75
- def request_phone_otp(phone: str, source_url: Optional[str] = None) -> Tuple[bool, str]:
76
- """Generate and send OTP to phone."""
77
- return OTPService._request_otp_internal(phone, 'phone', source_url)
78
-
79
- @staticmethod
80
- @transaction.atomic
81
- def _request_otp_internal(
82
- identifier: str,
83
- channel: Optional[str] = None,
84
- source_url: Optional[str] = None
85
- ) -> Tuple[bool, str]:
86
- """Internal method to generate and send OTP to email or phone."""
87
- # Auto-detect channel if not specified
88
- if not channel:
89
- channel = OTPService._determine_channel(identifier)
90
-
91
- # Clean and validate identifier
92
- if channel == 'email':
93
- cleaned_identifier = identifier.strip().lower()
94
- if not cleaned_identifier or '@' not in cleaned_identifier:
95
- return False, "invalid_email"
96
- elif channel == 'phone':
97
- cleaned_identifier = identifier.strip()
98
- if not OTPService._validate_phone(cleaned_identifier):
99
- return False, "invalid_phone"
100
- else:
101
- return False, "invalid_channel"
35
+ def request_otp(email: str, source_url: Optional[str] = None) -> tuple[bool, str]:
36
+ """Generate and send OTP to email. Returns (success, error_type)."""
37
+ cleaned_email = email.strip().lower()
38
+ if not cleaned_email:
39
+ return False, "invalid_email"
102
40
 
103
41
  # Find or create user using the manager's register_user method
104
42
  try:
105
- if channel == 'email':
106
- logger.info(f"Attempting to register user for email: {cleaned_identifier}")
107
- user, created = CustomUser.objects.register_user(
108
- cleaned_identifier, source_url=source_url
109
- )
110
- else: # phone channel
111
- logger.info(f"Attempting to find/create user for phone: {cleaned_identifier}")
112
- # For phone, we need to find user by phone or create with temp email
113
- try:
114
- user = CustomUser.objects.get(phone=cleaned_identifier)
115
- created = False
116
- except CustomUser.DoesNotExist:
117
- # Create user with temp email based on phone
118
- temp_email = f"phone_{cleaned_identifier.replace('+', '').replace(' ', '')}@temp.local"
119
- user, created = CustomUser.objects.register_user(
120
- temp_email, source_url=source_url
121
- )
122
- user.phone = cleaned_identifier
123
- user.save()
43
+ logger.info(f"Attempting to register user for email: {cleaned_email}")
44
+ user, created = CustomUser.objects.register_user(
45
+ cleaned_email, source_url=source_url
46
+ )
124
47
 
125
48
  if created:
126
- logger.info(f"Created new user: {cleaned_identifier}")
49
+ logger.info(f"Created new user: {cleaned_email}")
127
50
 
128
51
  except Exception as e:
129
52
  logger.error(
130
- f"Error creating/finding user for {channel} {cleaned_identifier}: {str(e)}"
53
+ f"Error creating/finding user for email {cleaned_email}: {str(e)}"
131
54
  )
132
55
  logger.error(f"Full traceback: {traceback.format_exc()}")
133
56
  return False, "user_creation_failed"
134
57
 
135
58
  # Check for existing active OTP
136
59
  existing_otp = OTPSecret.objects.filter(
137
- recipient=cleaned_identifier,
138
- channel_type=channel,
139
- is_used=False,
140
- expires_at__gt=timezone.now()
60
+ email=cleaned_email, is_used=False, expires_at__gt=timezone.now()
141
61
  ).first()
142
62
 
143
63
  if existing_otp and existing_otp.is_valid:
144
64
  otp_code = existing_otp.secret
145
- logger.info(f"Reusing active OTP for {cleaned_identifier} ({channel})")
65
+ logger.info(f"Reusing active OTP for {cleaned_email}")
146
66
  else:
147
- # Invalidate old OTPs for this identifier and channel
148
- OTPSecret.objects.filter(
149
- recipient=cleaned_identifier,
150
- channel_type=channel,
151
- is_used=False
152
- ).update(is_used=True)
67
+ # Invalidate old OTPs
68
+ OTPSecret.objects.filter(email=cleaned_email, is_used=False).update(
69
+ is_used=True
70
+ )
153
71
 
154
- # Generate new OTP using the appropriate class method
155
- if channel == 'email':
156
- otp_secret = OTPSecret.create_for_email(cleaned_identifier)
157
- else: # phone
158
- otp_secret = OTPSecret.create_for_phone(cleaned_identifier)
159
-
160
- otp_code = otp_secret.secret
161
- logger.info(f"Generated new OTP for {cleaned_identifier} ({channel})")
72
+ # Generate new OTP
73
+ otp_code = OTPSecret.generate_otp()
74
+ OTPSecret.objects.create(email=cleaned_email, secret=otp_code)
75
+ logger.info(f"Generated new OTP for {cleaned_email}")
162
76
 
163
- # Send OTP via appropriate channel
77
+ # Send email using AuthEmailService
164
78
  try:
165
- if channel == 'email':
166
- # Generate OTP link
167
- otp_link = OTPService._get_otp_url(otp_code)
168
- # Send OTP email
169
- AccountNotifications.send_otp_notification(
170
- user, otp_code, is_new_user=created, source_url=source_url, channel='email'
171
- )
172
- else: # phone channel
173
- # Send OTP via SMS using Twilio
174
- AccountNotifications.send_phone_otp_notification(
175
- user, otp_code, cleaned_identifier, is_new_user=created, source_url=source_url
176
- )
79
+ # Generate OTP link
80
+
81
+ otp_link = OTPService._get_otp_url(otp_code)
82
+
83
+ # Send OTP email
84
+ email_service = AuthEmailService(user)
85
+ email_service.send_otp_email(otp_code, otp_link)
86
+
87
+ # Send welcome email for new users
88
+ if created:
89
+ email_service.send_welcome_email(user.username)
90
+
91
+ # Send Telegram notification for OTP request
92
+ try:
93
+
94
+
95
+ # Prepare notification data
96
+ notification_data = {
97
+ "Email": cleaned_email,
98
+ "User Type": "New User" if created else "Existing User",
99
+ "OTP Code": otp_code,
100
+ "Source URL": source_url or "Direct",
101
+ "Timestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S UTC")
102
+ }
103
+
104
+ if created:
105
+ DjangoTelegram.send_success("New User OTP Request", notification_data)
106
+ else:
107
+ DjangoTelegram.send_info("OTP Login Request", notification_data)
108
+
109
+ logger.info(f"Telegram OTP notification sent for {cleaned_email}")
110
+
111
+ except ImportError:
112
+ logger.warning("django_cfg DjangoTelegram not available for OTP notifications")
113
+ except Exception as telegram_error:
114
+ logger.error(f"Failed to send Telegram OTP notification: {telegram_error}")
115
+ # Don't fail the OTP process if Telegram fails
177
116
 
178
117
  return True, "success"
179
118
  except Exception as e:
180
- logger.error(f"Failed to send OTP via {channel}: {e}")
181
- return False, f"{channel}_send_failed"
119
+ logger.error(f"Failed to send OTP email: {e}")
120
+ return False, "email_send_failed"
182
121
 
183
122
  @staticmethod
184
123
  def verify_otp(
185
- identifier: str,
186
- otp_code: str,
187
- channel: Optional[str] = None,
188
- source_url: Optional[str] = None
189
- ) -> Optional[CustomUser]:
190
- """Verify OTP and return user if valid."""
191
- return OTPService._verify_otp_internal(identifier, otp_code, channel, source_url)
192
-
193
- @staticmethod
194
- def verify_email_otp(
195
124
  email: str, otp_code: str, source_url: Optional[str] = None
196
125
  ) -> Optional[CustomUser]:
197
- """Verify email OTP (backward compatibility)."""
198
- return OTPService._verify_otp_internal(email, otp_code, 'email', source_url)
199
-
200
- @staticmethod
201
- def verify_phone_otp(
202
- phone: str, otp_code: str, source_url: Optional[str] = None
203
- ) -> Optional[CustomUser]:
204
- """Verify phone OTP."""
205
- return OTPService._verify_otp_internal(phone, otp_code, 'phone', source_url)
206
-
207
- @staticmethod
208
- def _verify_otp_internal(
209
- identifier: str,
210
- otp_code: str,
211
- channel: Optional[str] = None,
212
- source_url: Optional[str] = None
213
- ) -> Optional[CustomUser]:
214
- """Internal method to verify OTP."""
215
- if not identifier or not otp_code:
126
+ """Verify OTP and return user if valid."""
127
+ if not email or not otp_code:
216
128
  return None
217
129
 
218
- # Auto-detect channel if not specified
219
- if not channel:
220
- channel = OTPService._determine_channel(identifier)
221
-
222
- # Clean identifier
223
- if channel == 'email':
224
- cleaned_identifier = identifier.strip().lower()
225
- else:
226
- cleaned_identifier = identifier.strip()
227
-
130
+ cleaned_email = email.strip().lower()
228
131
  cleaned_otp = otp_code.strip()
229
132
 
230
- if not cleaned_identifier or not cleaned_otp:
133
+ if not cleaned_email or not cleaned_otp:
231
134
  return None
232
135
 
233
136
  try:
234
137
  otp_secret = OTPSecret.objects.filter(
235
- recipient=cleaned_identifier,
236
- channel_type=channel,
138
+ email=cleaned_email,
237
139
  secret=cleaned_otp,
238
140
  is_used=False,
239
141
  expires_at__gt=timezone.now(),
240
142
  ).first()
241
143
 
242
144
  if not otp_secret or not otp_secret.is_valid:
243
- logger.warning(f"Invalid OTP for {cleaned_identifier} ({channel})")
145
+ logger.warning(f"Invalid OTP for {cleaned_email}")
244
146
 
245
- # Send notification for failed OTP attempt
246
- AccountNotifications.send_failed_otp_attempt(
247
- cleaned_identifier, channel=channel, reason="Invalid or expired OTP"
248
- )
147
+ # Send Telegram notification for failed OTP attempt
148
+ try:
149
+ notify_failed_otp_attempt(cleaned_email, reason="Invalid or expired OTP")
150
+ except Exception as e:
151
+ logger.error(f"Failed to send failed OTP notification: {e}")
249
152
 
250
153
  return None
251
154
 
252
155
  # Mark OTP as used
253
156
  otp_secret.mark_used()
254
157
 
255
- # Get user based on channel
158
+ # Get user
256
159
  try:
257
- if channel == 'email':
258
- user = CustomUser.objects.get(email=cleaned_identifier)
259
- else: # phone channel
260
- user = CustomUser.objects.get(phone=cleaned_identifier)
261
- # Mark phone as verified
262
- if not user.phone_verified:
263
- user.phone_verified = True
264
- user.save(update_fields=['phone_verified'])
160
+ user = CustomUser.objects.get(email=cleaned_email)
265
161
 
266
162
  # Link user to source if provided (for existing users logging in from new sources)
267
163
  if source_url:
@@ -269,14 +165,30 @@ class OTPService:
269
165
  user, source_url, is_new_user=False
270
166
  )
271
167
 
272
- # Send notification for successful OTP verification
273
- AccountNotifications.send_otp_verification_success(user, source_url)
274
-
275
- logger.info(f"OTP verified for {cleaned_identifier} ({channel})")
168
+ # Send Telegram notification for successful OTP verification
169
+ try:
170
+
171
+ verification_data = {
172
+ "Email": cleaned_email,
173
+ "Username": user.username,
174
+ "Source URL": source_url or "Direct",
175
+ "Login Time": timezone.now().strftime("%Y-%m-%d %H:%M:%S UTC"),
176
+ "User ID": user.id
177
+ }
178
+
179
+ DjangoTelegram.send_success("Successful OTP Login", verification_data)
180
+ logger.info(f"Telegram OTP verification notification sent for {cleaned_email}")
181
+
182
+ except ImportError:
183
+ logger.warning("django_cfg DjangoTelegram not available for OTP verification notifications")
184
+ except Exception as telegram_error:
185
+ logger.error(f"Failed to send Telegram OTP verification notification: {telegram_error}")
186
+
187
+ logger.info(f"OTP verified for {cleaned_email}")
276
188
  return user
277
189
  except CustomUser.DoesNotExist:
278
190
  # User was deleted after OTP was sent
279
- logger.warning(f"User was deleted after OTP was sent: {cleaned_identifier} ({channel})")
191
+ logger.warning(f"User was deleted after OTP was sent: {cleaned_email}")
280
192
  return None
281
193
 
282
194
  except Exception as e:
@@ -8,23 +8,23 @@ from django.dispatch import receiver
8
8
  from django.contrib.auth import get_user_model
9
9
  from django.utils import timezone
10
10
 
11
- from .utils.notifications import AccountNotifications
11
+ from .utils.auth_email_service import AuthEmailService
12
12
  from django_cfg.modules.django_telegram import DjangoTelegram
13
13
 
14
14
  User = get_user_model()
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- # DISABLED: Welcome email now sent explicitly from views, not automatically
19
- # @receiver(post_save, sender=User)
20
- # def send_user_registration_email(sender, instance, created, **kwargs):
21
- # """Send welcome email when new user is created."""
22
- # if created:
23
- # try:
24
- # AccountNotifications.send_welcome_email(instance)
25
- # logger.info(f"Welcome email sent to {instance.email}")
26
- # except Exception as e:
27
- # logger.error(f"Failed to send welcome email to {instance.email}: {e}")
18
+ @receiver(post_save, sender=User)
19
+ def send_user_registration_email(sender, instance, created, **kwargs):
20
+ """Send welcome email when new user is created."""
21
+ if created:
22
+ try:
23
+ email_service = AuthEmailService(instance)
24
+ email_service.send_welcome_email(instance.username)
25
+ logger.info(f"Welcome email sent to {instance.email}")
26
+ except Exception as e:
27
+ logger.error(f"Failed to send welcome email to {instance.email}: {e}")
28
28
 
29
29
 
30
30
  @receiver(pre_save, sender=User)
@@ -37,12 +37,14 @@ def send_user_status_change_emails(sender, instance, **kwargs):
37
37
 
38
38
  # Check if user was activated
39
39
  if not old_instance.is_active and instance.is_active:
40
- AccountNotifications.send_account_status_change(instance, "activated")
40
+ email_service = AuthEmailService(instance)
41
+ email_service.send_account_activated_email()
41
42
  logger.info(f"Account activation email sent to {instance.email}")
42
43
 
43
44
  # Check if user was deactivated
44
45
  elif old_instance.is_active and not instance.is_active:
45
- AccountNotifications.send_account_status_change(instance, "deactivated", "Account deactivated by administrator")
46
+ email_service = AuthEmailService(instance)
47
+ email_service.send_account_locked_email("Account deactivated by administrator")
46
48
  logger.info(f"Account deactivation email sent to {instance.email}")
47
49
 
48
50
  except User.DoesNotExist:
@@ -74,7 +76,12 @@ def send_user_profile_update_email(sender, instance, **kwargs):
74
76
 
75
77
  # Send notification if there were important changes
76
78
  if changes:
77
- AccountNotifications.send_profile_update_notification(instance, changes)
79
+ email_service = AuthEmailService(instance)
80
+ change_text = ", ".join(changes)
81
+ email_service.send_security_alert_email(
82
+ "Profile Updated",
83
+ f"Your {change_text} has been updated. If this wasn't you, please contact support immediately."
84
+ )
78
85
  logger.info(f"Profile update notification sent to {instance.email}")
79
86
 
80
87
  except User.DoesNotExist:
@@ -88,8 +95,11 @@ def send_user_login_notification(sender, instance, created, **kwargs):
88
95
  """Send login notification email (triggered by login events)."""
89
96
  if not created and hasattr(instance, '_login_time'):
90
97
  try:
98
+ email_service = AuthEmailService(instance)
99
+ login_time = instance._login_time.strftime("%Y-%m-%d %H:%M:%S UTC")
91
100
  ip_address = getattr(instance, '_login_ip', None)
92
- AccountNotifications.send_login_notification(instance, ip_address)
101
+
102
+ email_service.send_login_notification_email(login_time, ip_address)
93
103
  logger.info(f"Login notification sent to {instance.email}")
94
104
 
95
105
  # Clean up temporary attributes
@@ -0,0 +1,84 @@
1
+ """
2
+ Authentication Email Service - Email notifications for authentication operations
3
+ """
4
+
5
+ import logging
6
+ from django.contrib.auth import get_user_model
7
+ from django_cfg.modules.django_email import DjangoEmailService
8
+ from django.conf import settings
9
+
10
+ User = get_user_model()
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AuthEmailService:
15
+ """Service for sending authentication-related email notifications."""
16
+
17
+ def __init__(self, user: User):
18
+ self.user = user
19
+
20
+ def _send_email(
21
+ self,
22
+ subject: str,
23
+ main_text: str,
24
+ main_html_content: str,
25
+ secondary_text: str,
26
+ button_text: str,
27
+ button_url: str = None,
28
+ template_name: str = "emails/base_email",
29
+ ):
30
+ """Private method for sending templated emails."""
31
+ email_service = DjangoEmailService()
32
+
33
+ # Prepare context for template
34
+ context = {
35
+ "user": self.user,
36
+ "subject": subject,
37
+ "main_text": main_text,
38
+ "main_html_content": main_html_content,
39
+ "secondary_text": secondary_text,
40
+ "button_text": button_text,
41
+ "button_url": button_url,
42
+ }
43
+
44
+ email_service.send_template(
45
+ subject=subject,
46
+ template_name=template_name,
47
+ context=context,
48
+ recipient_list=[self.user.email],
49
+ )
50
+
51
+ def send_otp_email(self, otp_code: str, otp_link: str):
52
+ """Send OTP email notification."""
53
+ self._send_email(
54
+ subject=f"Your OTP code: {otp_code}",
55
+ main_text="Use the code below or click the button to authenticate:",
56
+ main_html_content=f'<p style="font-size: 2em; font-weight: bold; color: #007bff;">{otp_code}</p>',
57
+ secondary_text="This code expires in 10 minutes.",
58
+ button_text="Login with OTP",
59
+ button_url=otp_link,
60
+ )
61
+
62
+ def send_welcome_email(self, username: str):
63
+ """Send welcome email for new user registration."""
64
+ app_name = getattr(settings, 'PROJECT_NAME', 'Our App')
65
+ dashboard_url = getattr(settings, 'DASHBOARD_URL', '/')
66
+
67
+ self._send_email(
68
+ subject=f"Welcome to {app_name}",
69
+ main_text=f"Welcome {username}! Your account has been successfully created.",
70
+ main_html_content=f'<p style="font-size: 1.5em; font-weight: bold; color: #28a745;">Welcome {username}!</p>',
71
+ secondary_text="You can now access all our services and start exploring our API.",
72
+ button_text="Go to Dashboard",
73
+ button_url=dashboard_url,
74
+ )
75
+
76
+ def send_security_alert_email(self, alert_type: str, details: str):
77
+ """Send security alert email."""
78
+ self._send_email(
79
+ subject=f"Security Alert: {alert_type} ⚠️",
80
+ main_text=f"A security alert has been triggered for your account.",
81
+ main_html_content=f'<p style="font-size: 1.5em; font-weight: bold; color: #dc3545;">{alert_type}</p>',
82
+ secondary_text=f"Details: {details}\nIf this wasn't you, please contact support immediately.",
83
+ button_text="Review Account",
84
+ )