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
-
146utils/__init__.py
-
10utils/exceptions.py
-
2utils/redis.py
@ -22,3 +22,9 @@ PHONENUMBER_DEFAULT_REGION=US |
|||
CENTRIFUGO_API_URL=http://centrifugo:8000/api |
|||
CENTRIFUGO_SECRET=super_secret_centrifugo_token_key_hmac_256 |
|||
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 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: |
|||
|
|||
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, |
|||
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 = { |
|||
'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) |
|||
# --------------------------------------------------------------------------- # |
|||
@ -48,3 +54,10 @@ CORS_ALLOW_ALL_ORIGINS = True |
|||
CENTRIFUGO_API_URL = 'http://localhost:8001/api' |
|||
CENTRIFUGO_SECRET = 'local-dev-hmac-secret' |
|||
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})") |
|||
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}>" |
|||
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}>" |
|||
|
|||
site_domain = getattr(settings, "SITE_DOMAIN", "https://dovodi.newhorizonco.uk/").rstrip('/') |
|||
subject = "Код подтверждения | Verification Code" |
|||
subject = f"Your Verification Code: {code}" |
|||
|
|||
html_content = f""" |
|||
<!DOCTYPE html> |
|||
<html lang="ru"> |
|||
# 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> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>Код подтверждения | Verification Code</title> |
|||
<title>Verification Code</title> |
|||
</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> |
|||
</html> |
|||
""" |
|||
</html>""" |
|||
|
|||
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 = { |
|||
@ -226,9 +182,9 @@ def send_email(recipient, code): |
|||
|
|||
try: |
|||
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: |
|||
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 |
|||
logger.info(f"Email OTP sent successfully via Resend to {to_emails}") |
|||
return True |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue