diff --git a/backend/Dockerfile b/backend/Dockerfile index 296927f..58b02e9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -21,5 +21,5 @@ COPY . /app/ EXPOSE 8000 # 7. ENTRYPOINT/CMD: กำหนด Entrypoint หลัก -ENTRYPOINT ["docker-entrypoint.sh"] +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file diff --git a/backend/Dockerfile.celery b/backend/Dockerfile.celery new file mode 100644 index 0000000..9f50a5d --- /dev/null +++ b/backend/Dockerfile.celery @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + libpq-dev \ + libffi-dev \ + libjpeg62-turbo-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["celery", "-A", "core", "worker", "-l", "info"] diff --git a/backend/accounts/emails.py b/backend/accounts/emails.py new file mode 100644 index 0000000..9f2142e --- /dev/null +++ b/backend/accounts/emails.py @@ -0,0 +1,81 @@ +from djoser import email +from django.conf import settings +from django.core.mail import EmailMultiAlternatives + +class CustomPasswordResetEmail(email.PasswordResetEmail): + + template_name = None + + def render(self, context=None): + + context = self.get_context_data() + url = context['url'] + user = context['user'] + + self.html_content = self.create_email_html(url, user) + self.plain_content = self.create_email_plain(url, user) + + def get_context_data(self): + return super().get_context_data() + + def get_subject(self): + """ + subject ถูกสร้างโดย Djoser ใน context['subject'] + """ + context = self.get_context_data() + return context.get("subject", "Password Reset") + + def send(self, to, *args, **kwargs): + + self.render() + + msg = EmailMultiAlternatives( + subject=self.get_subject(), + body=self.plain_content, + from_email=self.from_email, + to=to + ) + if self.html_content: + msg.attach_alternative(self.html_content, "text/html") + + # djcelery_email จะ Intercep เมธอด send() ของ EmailMultiAlternatives โดยอัตโนมัติ + return msg.send() + + def create_email_html(self, url, user): + # ดึง context data อีกครั้งเพื่อเข้าถึง protocol และ domain + context = self.get_context_data() + full_reset_url = f"{context['protocol']}://{context['domain']}{url}" # สร้าง Full URL + + return f""" + + + +

สวัสดีคุณ {user.username},

+

คุณได้ร้องขอการรีเซ็ตรหัสผ่าน โปรดคลิกลิงก์ด้านล่างเพื่อตั้งรหัสผ่านใหม่:

+ +

รีเซ็ตรหัสผ่าน (คลิกที่นี่)

+ +

หากลิงก์ไม่สามารถคลิกได้ กรุณาคัดลอกลิงก์นี้ไปวางในเบราว์เซอร์: {full_reset_url}

+

หากคุณไม่ได้ร้องขอ โปรดเพิกเฉยต่ออีเมลนี้

+

ขอบคุณครับ,
ทีม DDO Console

+ + + """ + + def create_email_plain(self, url, user): + # ดึง context data อีกครั้งเพื่อเข้าถึง protocol และ domain + context = self.get_context_data() + full_reset_url = f"{context['protocol']}://{context['domain']}{url}" # ⬅️ สร้าง Full URL + + return f""" + สวัสดี {user.username}, + + คุณได้ร้องขอการรีเซ็ตรหัสผ่าน กรุณาใช้ลิงก์ด้านล่างเพื่อตั้งรหัสผ่านใหม่: + + {full_reset_url} + + หากคุณไม่ได้ร้องขอ โปรดเพิกเฉยต่ออีเมลนี้ + + ขอบคุณครับ, + ทีม DDO Console + """ diff --git a/backend/core/celery.py b/backend/core/celery.py new file mode 100644 index 0000000..c421e06 --- /dev/null +++ b/backend/core/celery.py @@ -0,0 +1,16 @@ +import os +from celery import Celery + +# กำหนดค่า Django settings module ให้ Celery +# 'core.settings' คือ path ของ settings.py ของโปรเจกต์ Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +# สร้าง Celery application instance +app = Celery('core') # ชื่อตรงนี้ต้องตรงกับ -A core และ CELERY_APP: core + +# โหลด configuration จากไฟล์ settings.py ของ Django +# โดย Celery จะใช้ prefix CELERY_ (เช่น CELERY_BROKER_URL) +app.config_from_object('django.conf:settings', namespace='CELERY') + +# ค้นหา tasks ทั้งหมดใน INSTALLED_APPS ของ Django โดยอัตโนมัติ +app.autodiscover_tasks() \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index 409311b..1586f70 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -13,9 +13,11 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ from pathlib import Path import os -# ใน core/settings.py (ด้านบนสุด) -from dotenv import load_dotenv -load_dotenv() # โหลดตัวแปรจาก .env +try: + from dotenv import load_dotenv + load_dotenv() # โหลดตัวแปรจาก .env ใน Local Dev +except ImportError: + pass # ไม่ทำอะไรถ้า Module ไม่พบ (หมายความว่ารันอยู่ใน Docker) DB_HOST = os.getenv("DB_HOST", "cockroach-1") @@ -52,6 +54,7 @@ THIRD_PARTY_APPS = [ 'corsheaders', 'drf_spectacular', 'drf_spectacular_sidecar', + 'djcelery_email', ] LOCAL_APPS = [ @@ -206,12 +209,18 @@ REST_FRAMEWORK = { } # 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints) +DOMAIN = "localhost:5173" +SITE_NAME = 'localhost:5173' # หรือชื่อ Domain จริง DJOSER = { # ใช้งาน JWT โดยตรง 'USER_ID_FIELD': 'id', # ใช้ ID ของ User Model - 'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', # URL สำหรับ Frontend - 'USERNAME_RESET_CONFIRM_URL': '#/username/reset/confirm/{uid}/{token}', - 'ACTIVATION_URL': '#/activate/{uid}/{token}', # หากต้องการยืนยันอีเมล + 'EMAIL': { + # ชี้ไปยัง Custom Email Class ที่เราสร้าง + 'password_reset': 'accounts.emails.CustomPasswordResetEmail', + }, + 'PASSWORD_RESET_CONFIRM_URL': '/password/reset/confirm/{uid}/{token}', # URL ที่ Djoser จะใช้สร้างลิงก์ในอีเมล (ชี้ไปยัง Frontend Route) + 'USERNAME_RESET_CONFIRM_URL': '/username/reset/confirm/{uid}/{token}', + 'ACTIVATION_URL': '/activate/{uid}/{token}', # หากต้องการยืนยันอีเมล 'SERIALIZERS': { 'user_create': 'accounts.serializers.UserCreateSerializer', # จะสร้างในขั้นตอนถัดไป 'user': 'accounts.serializers.UserSerializer', # ใช้ UserSerializer เดิม @@ -220,7 +229,10 @@ DJOSER = { 'TOKEN_MODEL': None, # ไม่ใช้ TokenAuth แบบเก่า 'PERMISSIONS': { 'user_create': ['rest_framework.permissions.AllowAny'], - } + }, + "DOMAIN": "localhost:5173", + "SITE_NAME": "localhost:5173", + "PROTOCOL": "http", } REDIS_HOST = os.getenv("REDIS_HOST", "redis") @@ -248,8 +260,8 @@ SESSION_CACHE_ALIAS = "default" # CACHE_MIDDLEWARE_KEY_PREFIX = 'auth_cache' # CELERY CONFIGURATION -CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/0' -CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/0' +CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://redis:6379/0') +CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://redis:6379/0') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' @@ -310,4 +322,26 @@ SPECTACULAR_SETTINGS = { 'core.spectacular_hooks.rename_djoser_tags', ], # ตั้งค่าอื่น ๆ (ถ้าจำเป็น) -} \ No newline at end of file +} + +# ---------------------------------------------------------------------- +# CELERY EMAIL CONFIGURATION (Transactional Emails) +# ---------------------------------------------------------------------- + +# 1. EMAIL_BACKEND หลัก ใช้ Celery Email Backend +# Django จะส่งอีเมลทั้งหมดไปเข้าคิว Celery +EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' + +# 2. CELERY_EMAIL_BACKEND: กำหนด SMTP ที่ Celery Worker จะใช้ +# Celery Worker จะใช้ตัวนี้ในการส่งอีเมลออกจริง ๆ +CELERY_EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +# Mailjet SMTP Configuration (ใช้ Env Vars) +#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = os.getenv('MAILJET_SMTP_HOST') +EMAIL_PORT = os.getenv('MAILJET_SMTP_PORT') +EMAIL_USE_TLS = os.getenv('MAILJET_SMTP_TLS', 'True') == 'True' +EMAIL_HOST_USER = os.getenv('MAILJET_API_KEY') # API Key เป็น Username +EMAIL_HOST_PASSWORD = os.getenv('MAILJET_SECRET_KEY') # Secret Key เป็น Password +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL') # อีเมลผู้ส่ง +SERVER_EMAIL = DEFAULT_FROM_EMAIL # อีเมลสำหรับแจ้งเตือน Server \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index ff1c0a9..0aad1bb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,4 +13,7 @@ celery # ตัว Worker boto3 python-dotenv drf-spectacular -drf-spectacular-sidecar \ No newline at end of file +drf-spectacular-sidecar +django-celery-email +python-dotenv +flower \ No newline at end of file