diff --git a/back-end/account/email.py b/back-end/account/email.py index bd01951..92c24c4 100644 --- a/back-end/account/email.py +++ b/back-end/account/email.py @@ -2,6 +2,7 @@ from django.template.loader import render_to_string from django.utils.html import strip_tags from django.core.mail import EmailMultiAlternatives +from django.conf import settings from djoser.conf import settings as djoser_settings from urllib.parse import urlparse @@ -21,7 +22,16 @@ def build_frontend_url(path: str): prefix = "/".join(parts[:-2]) - domain = djoser_settings.DOMAIN + domain = getattr(djoser_settings, "DOMAIN", None) + if not domain: + frontend_url = getattr(settings, "FRONTEND_URL", "") + parsed_frontend = urlparse(frontend_url) + domain = parsed_frontend.netloc or frontend_url + + if not domain: + raise ValueError( + "Unable to construct email link: set DJOSER.DOMAIN or PUBLIC_APP_URL/FRONTEND_URL." + ) return f"https://{domain}/{prefix}/{uid}/{token}" diff --git a/back-end/core/settings/base.py b/back-end/core/settings/base.py index 317d5c1..67c0919 100644 --- a/back-end/core/settings/base.py +++ b/back-end/core/settings/base.py @@ -9,6 +9,7 @@ from pathlib import Path import os +from urllib.parse import urlparse import dj_database_url from dotenv import load_dotenv import cloudinary @@ -50,6 +51,7 @@ "djoser", "corsheaders", "drf_spectacular", + "anymail", "cloudinary_storage", "cloudinary", ] @@ -217,6 +219,8 @@ "USER_CREATE_PASSWORD_RETYPE": True, "ACTIVATION_URL": "verify-email/{uid}/{token}", "PASSWORD_RESET_CONFIRM_URL": "reset-password/{uid}/{token}", + "DOMAIN": urlparse(os.environ.get("PUBLIC_APP_URL", "http://localhost:5173")).netloc + or os.environ.get("PUBLIC_APP_URL", "localhost:5173"), "SERIALIZERS": { "user_create": "account.serializers.UserCreateSerializer", "user": "account.serializers.UserSerializer", @@ -225,6 +229,10 @@ "VIEWS": { "user_create": "account.views.CustomUserViewSet" }, + "EMAIL": { + "activation": "account.email.CustomActivationEmail", + "password_reset": "account.email.CustomPasswordResetEmail", + }, } # ─── CORS ─────────────────────────────────────────────────── @@ -254,13 +262,21 @@ GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "").strip() # ─── Email ────────────────────────────────────────────────── -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY", "") +ANYMAIL = { + "SENDGRID_API_KEY": SENDGRID_API_KEY, +} +EMAIL_BACKEND = ( + "anymail.backends.sendgrid.EmailBackend" + if SENDGRID_API_KEY + else "django.core.mail.backends.smtp.EmailBackend" +) EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com") EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587)) EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "True") == "True" EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") -DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@onlinetherapy.com") +DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_FROM", os.environ.get("DEFAULT_FROM_EMAIL", "noreply@onlinetherapy.com")) # Frontend URL (for email links) FRONTEND_URL = os.environ.get("PUBLIC_APP_URL", "http://localhost:5173") diff --git a/back-end/core/settings/development.py b/back-end/core/settings/development.py index 51367b8..382143d 100644 --- a/back-end/core/settings/development.py +++ b/back-end/core/settings/development.py @@ -1,10 +1,15 @@ """Development Settings""" from .base import * # noqa: F401, F403 +import os DEBUG = True -# Show emails in console during development -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +# Use console backend when SendGrid is not configured in development. +EMAIL_BACKEND = ( + "anymail.backends.sendgrid.EmailBackend" + if os.environ.get("SENDGRID_API_KEY") + else "django.core.mail.backends.console.EmailBackend" +) # Django Debug Toolbar (optional but useful) INSTALLED_APPS += ["django_extensions"] # noqa: F405 diff --git a/back-end/pyproject.toml b/back-end/pyproject.toml index f69be22..f10c9ee 100644 --- a/back-end/pyproject.toml +++ b/back-end/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "groq>=0.5.0", "cloudinary>=1.44.2", "django-cloudinary-storage>=0.3.0", + "django-anymail[sendgrid]>=9.4.0", ] [project.optional-dependencies] diff --git a/back-end/uv.lock b/back-end/uv.lock index 99bc525..aa90eb5 100644 --- a/back-end/uv.lock +++ b/back-end/uv.lock @@ -236,6 +236,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/32/4b144e125678efccf5d5b61581de1c4088d6b0286e46096e3b8de0d556c8/django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7", size = 8310245, upload-time = "2026-03-03T13:56:01.174Z" }, ] +[[package]] +name = "django-anymail" +version = "15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "idna" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/43/f0aadb31f2c58afcd9f001f4291998cbd6d289898167e79d908506fc6faf/django_anymail-15.0.tar.gz", hash = "sha256:23d8ab6589afe8cc1ae7665c26879814ad192f4c3ed837a2a1868b0a056869e0", size = 106985, upload-time = "2026-04-18T20:44:19.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d1/daae99ec3b30886010a499975880ec20c32c622bee6b92c226b715e42f0c/django_anymail-15.0-py3-none-any.whl", hash = "sha256:64d33dd1084bfc8e4e12245f56629be40aa0b0498fc7fc7544d87b9b2048be1e", size = 147229, upload-time = "2026-04-18T20:44:17.323Z" }, +] + +[package.optional-dependencies] +sendgrid = [ + { name = "cryptography" }, +] + [[package]] name = "django-cloudinary-storage" version = "0.3.0" @@ -607,6 +627,7 @@ dependencies = [ { name = "cryptography" }, { name = "dj-database-url" }, { name = "django" }, + { name = "django-anymail", extra = ["sendgrid"] }, { name = "django-cloudinary-storage" }, { name = "django-cors-headers" }, { name = "django-filter" }, @@ -640,6 +661,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=42.0.0" }, { name = "dj-database-url", specifier = ">=2.1.0" }, { name = "django", specifier = ">=5.1" }, + { name = "django-anymail", extras = ["sendgrid"], specifier = ">=9.4.0" }, { name = "django-cloudinary-storage", specifier = ">=0.3.0" }, { name = "django-cors-headers", specifier = ">=4.3.1" }, { name = "django-filter", specifier = ">=25.2" },