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