django-cfg 1.1.82__py3-none-any.whl → 1.2.1__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.
- django_cfg/__init__.py +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +450 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py → core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +91 -19
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/METADATA +83 -86
- django_cfg-1.2.1.dist-info/RECORD +441 -0
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.82.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py → utils/version_check.py} +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.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
|
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.
|
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
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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: {
|
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
|
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
|
-
|
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 {
|
65
|
+
logger.info(f"Reusing active OTP for {cleaned_email}")
|
146
66
|
else:
|
147
|
-
# Invalidate old OTPs
|
148
|
-
OTPSecret.objects.filter(
|
149
|
-
|
150
|
-
|
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
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
77
|
+
# Send email using AuthEmailService
|
164
78
|
try:
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
181
|
-
return False,
|
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
|
198
|
-
|
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
|
-
|
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
|
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
|
-
|
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 {
|
145
|
+
logger.warning(f"Invalid OTP for {cleaned_email}")
|
244
146
|
|
245
|
-
# Send notification for failed OTP attempt
|
246
|
-
|
247
|
-
|
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
|
158
|
+
# Get user
|
256
159
|
try:
|
257
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
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: {
|
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.
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
+
)
|