Browse Source
feat: add project base settings, Redis configuration, and user authentication endpoints
main
feat: add project base settings, Redis configuration, and user authentication endpoints
main
13 changed files with 200 additions and 132 deletions
-
6.env.dev
-
5apps/account/custom_user_login.py
-
34apps/account/serializers/auth_serializers.py
-
4apps/account/urls_auth.py
-
9apps/account/views/auth_views.py
-
72apps/account/views/user.py
-
14config/redis_config.py
-
6config/settings/base.py
-
17config/settings/local.py
-
7manage.py
-
150utils/__init__.py
-
10utils/exceptions.py
-
2utils/redis.py
@ -22,3 +22,9 @@ PHONENUMBER_DEFAULT_REGION=US |
|||||
CENTRIFUGO_API_URL=http://centrifugo:8000/api |
CENTRIFUGO_API_URL=http://centrifugo:8000/api |
||||
CENTRIFUGO_SECRET=super_secret_centrifugo_token_key_hmac_256 |
CENTRIFUGO_SECRET=super_secret_centrifugo_token_key_hmac_256 |
||||
CENTRIFUGO_API_KEY=centrifugo_internal_api_access_key |
CENTRIFUGO_API_KEY=centrifugo_internal_api_access_key |
||||
|
|
||||
|
# Resend Email Service |
||||
|
RESEND_API_KEY=re_7rkP5MPK_9gLDwdw1f23hzP3wsv2F6A8e |
||||
|
[email protected] |
||||
|
|
||||
|
|
||||
@ -1,15 +1,21 @@ |
|||||
from redis import Redis, ConnectionPool |
from redis import Redis, ConnectionPool |
||||
|
from django.conf import settings |
||||
|
|
||||
from config.settings import base as settings |
|
||||
|
_pool = None |
||||
|
|
||||
|
|
||||
|
|
||||
pool = ConnectionPool.from_url(url= settings.REDIS_URL,max_connections=100) |
|
||||
|
def get_redis_pool(): |
||||
|
global _pool |
||||
|
if _pool is None: |
||||
|
redis_url = getattr(settings, 'REDIS_URL', 'redis://127.0.0.1:6379/0') |
||||
|
_pool = ConnectionPool.from_url(url=redis_url, max_connections=100) |
||||
|
return _pool |
||||
|
|
||||
|
|
||||
class RedisConfig: |
class RedisConfig: |
||||
|
|
||||
def __init__(self): |
def __init__(self): |
||||
self.redis = Redis(connection_pool=pool, decode_responses=True) |
|
||||
|
self.redis = Redis(connection_pool=get_redis_pool(), decode_responses=True) |
||||
|
|
||||
|
|
||||
|
|
||||
@ -413,3 +413,9 @@ if SENTRY_DSN: |
|||||
dsn=SENTRY_DSN, |
dsn=SENTRY_DSN, |
||||
send_default_pii=True, |
send_default_pii=True, |
||||
) |
) |
||||
|
|
||||
|
# Resend Email Service |
||||
|
RESEND_API_KEY = env('RESEND_API_KEY', default='') |
||||
|
RESEND_FROM_EMAIL = env('RESEND_FROM_EMAIL', default='[email protected]') |
||||
|
|
||||
|
|
||||
@ -16,14 +16,20 @@ DATABASES = { |
|||||
} |
} |
||||
|
|
||||
# --------------------------------------------------------------------------- # |
# --------------------------------------------------------------------------- # |
||||
# Cache: local-memory (no Redis needed) |
|
||||
|
# Redis & Cache (local Redis service on port 6379) |
||||
# --------------------------------------------------------------------------- # |
# --------------------------------------------------------------------------- # |
||||
|
REDIS_URL = 'redis://127.0.0.1:6379/0' |
||||
CACHES = { |
CACHES = { |
||||
'default': { |
'default': { |
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', |
|
||||
|
'BACKEND': 'django_redis.cache.RedisCache', |
||||
|
'LOCATION': REDIS_URL, |
||||
|
'OPTIONS': { |
||||
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient', |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
|
|
||||
# --------------------------------------------------------------------------- # |
# --------------------------------------------------------------------------- # |
||||
# Celery: run tasks eagerly in-process (no broker needed) |
# Celery: run tasks eagerly in-process (no broker needed) |
||||
# --------------------------------------------------------------------------- # |
# --------------------------------------------------------------------------- # |
||||
@ -48,3 +54,10 @@ CORS_ALLOW_ALL_ORIGINS = True |
|||||
CENTRIFUGO_API_URL = 'http://localhost:8001/api' |
CENTRIFUGO_API_URL = 'http://localhost:8001/api' |
||||
CENTRIFUGO_SECRET = 'local-dev-hmac-secret' |
CENTRIFUGO_SECRET = 'local-dev-hmac-secret' |
||||
CENTRIFUGO_API_KEY = 'local-dev-api-key' |
CENTRIFUGO_API_KEY = 'local-dev-api-key' |
||||
|
|
||||
|
# Resend Email Service |
||||
|
RESEND_API_KEY = env('RESEND_API_KEY', default='re_7rkP5MPK_9gLDwdw1f23hzP3wsv2F6A8e') |
||||
|
RESEND_FROM_EMAIL = env('RESEND_FROM_EMAIL', default='[email protected]') |
||||
|
RESEND_SANDBOX_REDIRECT_TO = env('RESEND_SANDBOX_REDIRECT_TO', default='[email protected]') |
||||
|
|
||||
|
|
||||
@ -112,108 +112,64 @@ def send_email(recipient, code): |
|||||
logger.error(f"Cannot send email: recipient list is empty ({recipient})") |
logger.error(f"Cannot send email: recipient list is empty ({recipient})") |
||||
return False |
return False |
||||
|
|
||||
from_email = getattr(settings, "RESEND_FROM_EMAIL", "Dovodi <[email protected]>") |
|
||||
if "<" not in from_email and "@" in from_email: |
|
||||
from_email = f"Dovodi <{from_email}>" |
|
||||
|
|
||||
site_domain = getattr(settings, "SITE_DOMAIN", "https://dovodi.newhorizonco.uk/").rstrip('/') |
|
||||
subject = "Код подтверждения | Verification Code" |
|
||||
|
|
||||
html_content = f""" |
|
||||
<!DOCTYPE html> |
|
||||
<html lang="ru"> |
|
||||
|
from_email = getattr(settings, "RESEND_FROM_EMAIL", "[email protected]") |
||||
|
if "[email protected]" in from_email: |
||||
|
from_email = "[email protected]" |
||||
|
elif "<" not in from_email and "@" in from_email: |
||||
|
from_email = f"Muslim Connect Hub <{from_email}>" |
||||
|
|
||||
|
subject = f"Your Verification Code: {code}" |
||||
|
|
||||
|
# Handle Resend sandbox restriction: [email protected] can only deliver to the account owner's email |
||||
|
sandbox_redirect = getattr(settings, "RESEND_SANDBOX_REDIRECT_TO", "[email protected]") |
||||
|
sandbox_notice_html = "" |
||||
|
sandbox_notice_text = "" |
||||
|
|
||||
|
if "[email protected]" in from_email and sandbox_redirect: |
||||
|
non_sandbox_recipients = [r for r in to_emails if r.lower() != sandbox_redirect.lower()] |
||||
|
if non_sandbox_recipients: |
||||
|
orig_str = ", ".join(to_emails) |
||||
|
print(f"[Resend Sandbox Mode] Notice: Resend sandbox only delivers to {sandbox_redirect}.") |
||||
|
print(f"[Resend Sandbox Mode] Redirecting test email intended for [{orig_str}] -> {sandbox_redirect}") |
||||
|
subject = f"[Dev Test for {orig_str}] Your Verification Code: {code}" |
||||
|
sandbox_notice_html = f""" |
||||
|
<div style="background-color: #FEF3C7; border: 1px solid #F59E0B; border-radius: 8px; padding: 12px; margin-bottom: 20px; font-size: 13px; color: #92400E; text-align: left;"> |
||||
|
<strong>Sandbox Redirection Notice:</strong><br/> |
||||
|
This email was intended for <code>{orig_str}</code>. Because Resend's free tier allows delivery only to the verified account email, it was routed to <code>{sandbox_redirect}</code> for testing. |
||||
|
</div> |
||||
|
""" |
||||
|
sandbox_notice_text = f"[Notice: Resend test sandbox redirecting from {orig_str} to {sandbox_redirect}]\n\n" |
||||
|
to_emails = [sandbox_redirect] |
||||
|
|
||||
|
html_content = f"""<!DOCTYPE html> |
||||
|
<html> |
||||
<head> |
<head> |
||||
<meta charset="UTF-8"> |
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>Код подтверждения | Verification Code</title> |
|
||||
|
<title>Verification Code</title> |
||||
</head> |
</head> |
||||
<body style="margin: 0; padding: 0; background-color: #F4F6F9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #15171C; -webkit-font-smoothing: antialiased;"> |
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed; background-color: #F4F6F9; padding: 40px 16px;"> |
|
||||
<tr> |
|
||||
<td align="center" valign="top"> |
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 540px; background-color: #FFFFFF; border-radius: 16px; border: 1px solid #E2E8F0; box-shadow: 0 4px 20px -2px rgba(21, 23, 28, 0.06); overflow: hidden;"> |
|
||||
<!-- Top Gradient Accent Line --> |
|
||||
<tr> |
|
||||
<td style="height: 4px; background: linear-gradient(90deg, #5172E1 0%, #1C458C 100%); line-height: 4px; font-size: 4px;"> </td> |
|
||||
</tr> |
|
||||
<!-- Header with Logo --> |
|
||||
<tr> |
|
||||
<td style="padding: 32px 32px 20px 32px; text-align: center; background-color: #FAFAFC; border-bottom: 1px solid #F1F3F7;"> |
|
||||
<img src="{site_domain}/static/images/dovoodi_logo.svg" alt="Dovodi" style="height: 38px; width: auto; max-width: 160px; display: inline-block;" /> |
|
||||
</td> |
|
||||
</tr> |
|
||||
<!-- Main Body Section --> |
|
||||
<tr> |
|
||||
<td style="padding: 36px 36px 28px 36px;"> |
|
||||
<!-- Security Badge --> |
|
||||
<div style="text-align: center; margin-bottom: 16px;"> |
|
||||
<span style="display: inline-block; background-color: #EEF2FF; color: #3B66DE; border: 1px solid #D5DEFF; padding: 4px 12px; border-radius: 9999px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;"> |
|
||||
Безопасность / Security |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Main Title --> |
|
||||
<h1 style="margin: 0 0 12px 0; font-size: 22px; font-weight: 800; color: #15171C; text-align: center; line-height: 1.3;"> |
|
||||
Код подтверждения |
|
||||
</h1> |
|
||||
|
|
||||
<!-- Intro Message --> |
|
||||
<p style="margin: 0 0 24px 0; font-size: 14px; color: #646A75; text-align: center; line-height: 1.6;"> |
|
||||
Используйте следующий одноразовый код для входа یا подтверждения учетной записи в <strong>Dovodi</strong>: |
|
||||
</p> |
|
||||
|
|
||||
<!-- OTP Box --> |
|
||||
<div style="text-align: center; margin: 28px 0; padding: 22px; background-color: #F8FAFD; border: 1.5px dashed #5172E1; border-radius: 14px;"> |
|
||||
<span style="font-family: 'SF Pro Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 38px; font-weight: 800; color: #15171C; letter-spacing: 10px; display: inline-block;"> |
|
||||
{code} |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Expiry & Security Notice --> |
|
||||
<div style="background-color: #FFFBEB; border: 1px solid #FDE68A; border-radius: 10px; padding: 12px 16px; margin-bottom: 24px;"> |
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%"> |
|
||||
<tr> |
|
||||
<td style="font-size: 12px; color: #92400E; line-height: 1.5;"> |
|
||||
⏱ <strong>Внимание:</strong> Срок действия кода истекает через 5 минут. Если вы не запрашивали этот код, просто проигнорируйте это письмо. |
|
||||
</td> |
|
||||
</tr> |
|
||||
</table> |
|
||||
</div> |
|
||||
|
|
||||
<p style="margin: 0; font-size: 13px; color: #8C93A0; text-align: center; line-height: 1.5;"> |
|
||||
Никому не передавайте этот код в целях безопасности вашего аккаунта. |
|
||||
</p> |
|
||||
</td> |
|
||||
</tr> |
|
||||
<!-- Footer Section --> |
|
||||
<tr> |
|
||||
<td style="padding: 24px 36px; background-color: #F8F9FB; border-top: 1px solid #EEF0F4; text-align: center;"> |
|
||||
<p style="margin: 0 0 6px 0; font-size: 13px; font-weight: 700; color: #15171C;"> |
|
||||
Dovodi |
|
||||
</p> |
|
||||
<p style="margin: 0 0 12px 0; font-size: 12px; color: #646A75;"> |
|
||||
<a href="https://dovodi.newhorizonco.uk/" style="color: #5172E1; text-decoration: none; font-weight: 600;">dovodi.newhorizonco.uk</a> |
|
||||
• |
|
||||
<a href="mailto:[email protected]" style="color: #5172E1; text-decoration: none; font-weight: 600;">info@imamjavad.online</a> |
|
||||
</p> |
|
||||
<p style="margin: 0; font-size: 11px; color: #9AA0AC; line-height: 1.4;"> |
|
||||
© 2026 Dovodi. Все права защищены. |
|
||||
</p> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</table> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</table> |
|
||||
|
<body style="margin: 0; padding: 40px 16px; background-color: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #0F172A;"> |
||||
|
<div style="max-width: 480px; margin: 0 auto; background: #FFFFFF; border-radius: 12px; border: 1px solid #E2E8F0; padding: 32px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);"> |
||||
|
{sandbox_notice_html} |
||||
|
<h2 style="margin: 0 0 16px; color: #1E293B; font-size: 22px;">Muslim Connect Hub</h2> |
||||
|
<p style="margin: 0 0 24px; color: #64748B; font-size: 15px; line-height: 1.5;"> |
||||
|
Use the following verification code to complete your registration: |
||||
|
</p> |
||||
|
<div style="background-color: #F1F5F9; border-radius: 8px; padding: 18px 24px; margin: 24px 0; display: inline-block;"> |
||||
|
<span style="font-family: monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #0284C7;"> |
||||
|
{code} |
||||
|
</span> |
||||
|
</div> |
||||
|
<p style="margin: 24px 0 0; color: #94A3B8; font-size: 13px;"> |
||||
|
This code will expire in 5 minutes. If you did not request this code, please ignore this email. |
||||
|
</p> |
||||
|
</div> |
||||
</body> |
</body> |
||||
</html> |
|
||||
""" |
|
||||
|
</html>""" |
||||
|
|
||||
text_content = ( |
text_content = ( |
||||
f"Код подтверждения Dovodi: {code}\n\n" |
|
||||
f"Your Dovodi verification code is: {code}\n\n" |
|
||||
f"Срок действия кода истекает через 5 минут. Если вы не запрашивали этот код, проигнорируйте это письмо.\n" |
|
||||
f"Никому не передавайте этот код в целях безопасности вашего аккаунта." |
|
||||
|
f"{sandbox_notice_text}" |
||||
|
f"Muslim Connect Hub Verification Code: {code}\n\n" |
||||
|
f"This code will expire in 5 minutes. If you did not request this code, please ignore this email." |
||||
) |
) |
||||
|
|
||||
payload = { |
payload = { |
||||
@ -226,9 +182,9 @@ def send_email(recipient, code): |
|||||
|
|
||||
try: |
try: |
||||
response = requests.post(url, headers=headers, json=payload, timeout=15) |
response = requests.post(url, headers=headers, json=payload, timeout=15) |
||||
|
print(f"[Resend API] status={response.status_code} response={response.text}") |
||||
if response.status_code >= 400: |
if response.status_code >= 400: |
||||
logger.error(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}") |
logger.error(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}") |
||||
print(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}") |
|
||||
return False |
return False |
||||
logger.info(f"Email OTP sent successfully via Resend to {to_emails}") |
logger.info(f"Email OTP sent successfully via Resend to {to_emails}") |
||||
return True |
return True |
||||
|
|||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue