Skip to content
Merged

Dev #33

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
38e6b76
fix: remove old image from s3 when updating or deleting image
ngovinh2k2 Jun 8, 2026
1153d34
Merge pull request #23 from Space-DF/fix/remove-old-image-from-s3-whe…
ngovinh2k2 Jun 8, 2026
4e78ca9
feat: implement api crud for auth pages and email
ngovinh2k2 Jul 2, 2026
e5b019c
Merge pull request #24 from Space-DF/feat/implement-api-crud-for-auth…
ngovinh2k2 Jul 2, 2026
8c8ac0e
feat: implement api crud for brand and theme color
ngovinh2k2 Jul 2, 2026
6510122
Merge pull request #25 from Space-DF/feat/implement-api-crud-for-bran…
ngovinh2k2 Jul 2, 2026
ce3f6b7
feat: implement custom emails models
ngovinh2k2 Jul 2, 2026
b8c18a1
Merge pull request #26 from Space-DF/feat/implement-custom-emails-models
ngovinh2k2 Jul 2, 2026
5807e5b
feat: update the email ui in the template folder
ngovinh2k2 Jul 3, 2026
6c6a103
Merge pull request #27 from Space-DF/feat/update-the-email-ui-in-the-…
ngovinh2k2 Jul 3, 2026
8438cf5
fix: remove logo and favicon default
ngovinh2k2 Jul 3, 2026
d45dd7a
Merge pull request #28 from Space-DF/fix/remove-logo-and-favicon-default
ngovinh2k2 Jul 3, 2026
94c394d
feat: implement django silk for api performance monitoring and profiling
ngovinh2k2 Jul 3, 2026
5a9fcc2
Merge pull request #29 from Space-DF/feat/implement-django-silk-for-a…
ngovinh2k2 Jul 3, 2026
5be3410
fix: rename url for silk
ngovinh2k2 Jul 3, 2026
50e475c
Merge pull request #30 from Space-DF/fix/rename-url-for-silk
ngovinh2k2 Jul 3, 2026
25b3f98
chore: improve query performance
ngovinh2k2 Jul 3, 2026
e4473e2
Merge pull request #31 from Space-DF/chore/improve-query-performance
ngovinh2k2 Jul 3, 2026
c0998f9
fix: update response for api check org
ngovinh2k2 Jul 3, 2026
1daa611
Merge pull request #32 from Space-DF/fix/update-response-for-api-chec…
ngovinh2k2 Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions apps/authentication/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
from common.apps.refresh_tokens.services import create_jwt_tokens
from django.conf import settings
from django.core.cache import cache
from django.template.loader import render_to_string
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response

from apps.authentication.models import RootUser
Expand Down Expand Up @@ -92,14 +90,3 @@ def handle_access_token(access_token, provider: Literal["GOOGLE"]):
"default_organization": default_organization_slug,
},
)


def render_email_format(template, data):
try:
html_message = render_to_string(
template,
data,
)
return html_message
except Exception as e:
raise ValidationError({"error": f"Error: {e}"})
22 changes: 13 additions & 9 deletions apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timezone

from common.apps.refresh_tokens.serializers import TokenPairSerializer
from common.utils.email_context import get_email_context, render_email_format
from common.utils.send_email import send_email
from common.utils.token_jwt import generate_token
from django.conf import settings
Expand All @@ -22,10 +23,9 @@
SendEmailSerializer,
UserSerializer,
)
from apps.authentication.services import (
create_organization_access_token,
render_email_format,
)
from apps.authentication.services import create_organization_access_token
from apps.custom_email.constants import EmailTypes
from apps.custom_email.service import get_custom_email_item


class LoginAPIView(TokenObtainPairView):
Expand Down Expand Up @@ -69,11 +69,15 @@ def post(self, request):

subject = "🔒 Forgot your password? Reset now"
token = generate_token({"email": email})
data = {
"redirect_url": f"{settings.HOST_FRONTEND_ADMIN}/auth/reset-password?token={token}",
"host": settings.HOST,
}
message = render_email_format("email_forget_password.html", data)
custom_email_item = get_custom_email_item(EmailTypes.RESET_PASSWORD)
email_context = get_email_context(
{
"host": settings.HOST,
"redirect_url": f"{settings.HOST_FRONTEND_ADMIN}/auth/reset-password?token={token}",
},
custom_email=custom_email_item,
)
message = render_email_format("email_forget_password.html", email_context)
send_email(settings.DEFAULT_FROM_EMAIL, [email], subject, message)
return Response(
{
Expand Down
Empty file added apps/custom_email/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions apps/custom_email/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CustomEmailConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.custom_email"
7 changes: 7 additions & 0 deletions apps/custom_email/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.db import models


class EmailTypes(models.TextChoices):
INVITATION_TO_SPACE = "invitation_to_space"
VERIFICATION_CODE = "verification_code"
RESET_PASSWORD = "reset_password" # nosec B105
72 changes: 72 additions & 0 deletions apps/custom_email/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 5.0.6 on 2026-06-01 00:00

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
("organization", "0004_alter_organization_template"),
]

operations = [
migrations.CreateModel(
name="OrganizationEmail",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"email_type",
models.CharField(
choices=[
("invitation_to_space", "Invitation To Space"),
("verification_code", "Verification Code"),
("reset_password", "Reset Password"),
],
max_length=255,
),
),
("sender_name", models.CharField(blank=True, max_length=255)),
("sender_email", models.CharField(blank=True, max_length=255)),
("theme_colors", models.JSONField(blank=True, default=dict)),
("footer_text", models.TextField(blank=True)),
(
"header_image",
models.CharField(blank=True, default="", max_length=500),
),
("show_logo", models.BooleanField(default=True)),
("social_links", models.JSONField(blank=True, default=dict)),
("metadata", models.JSONField(blank=True, default=dict)),
(
"organization",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="organization_custom_emails",
to="organization.organization",
),
),
],
options={
"db_table": "custom_emails",
"constraints": [
models.UniqueConstraint(
fields=("organization", "email_type"),
name="unique_organization_email_type",
)
],
},
),
]
43 changes: 43 additions & 0 deletions apps/custom_email/migrations/0002_backfill_organization_emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.db import migrations

from apps.custom_email.service import get_default_organization_emails

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Importing from apps.custom_email.service at the top level of a migration is a Django anti-pattern. The service file imports live models (e.g., OrganizationEmail), which can cause migration failures in the future if those models are modified or deleted. Consider moving the default email data to a standalone constants file that does not import any models, or defining the default data directly within the migration.



def backfill_organization_emails(apps, schema_editor):
Organization = apps.get_model("organization", "Organization")
OrganizationEmail = apps.get_model("custom_email", "OrganizationEmail")

existing_pairs = set(
OrganizationEmail.objects.values_list("organization_id", "email_type")
)
emails_to_create = []
default_emails = get_default_organization_emails()

for organization in Organization.objects.all().iterator():
for email_data in default_emails:
email_type = email_data["email_type"]
if (organization.id, email_type) in existing_pairs:
continue

emails_to_create.append(
OrganizationEmail(
organization_id=organization.id,
**email_data,
)
)

if emails_to_create:
OrganizationEmail.objects.bulk_create(emails_to_create, batch_size=100)


class Migration(migrations.Migration):
dependencies = [
("custom_email", "0001_initial"),
]

operations = [
migrations.RunPython(
backfill_organization_emails,
migrations.RunPython.noop,
),
]
1 change: 1 addition & 0 deletions apps/custom_email/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

31 changes: 31 additions & 0 deletions apps/custom_email/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from common.models.base_model import BaseModel
from django.db import models

from apps.custom_email.constants import EmailTypes
from apps.organization.models import Organization


class OrganizationEmail(BaseModel):
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="organization_custom_emails",
)
email_type = models.CharField(max_length=255, choices=EmailTypes.choices)
sender_name = models.CharField(max_length=255, blank=True)
sender_email = models.CharField(max_length=255, blank=True)
theme_colors = models.JSONField(default=dict, blank=True)
footer_text = models.TextField(blank=True)
header_image = models.CharField(max_length=500, blank=True, default="")
show_logo = models.BooleanField(default=True)
social_links = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)

class Meta:
db_table = "custom_emails"
constraints = [
models.UniqueConstraint(
fields=["organization", "email_type"],
name="unique_organization_email_type",
)
]
76 changes: 76 additions & 0 deletions apps/custom_email/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from common.apps.upload_file.service import get_presigned_url
from django.conf import settings
from rest_framework import serializers

from apps.custom_email.constants import EmailTypes
from apps.custom_email.models import OrganizationEmail
from apps.custom_email.service import get_theme_logo_url


class OrganizationEmailSerializer(serializers.ModelSerializer):
brand_logo_dark = serializers.SerializerMethodField()
brand_logo_light = serializers.SerializerMethodField()
brand_name = serializers.SerializerMethodField()

class Meta:
model = OrganizationEmail
fields = [
"id",
"brand_logo_dark",
"brand_logo_light",
"brand_name",
"email_type",
"email_type",
Comment on lines +22 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The email_type field is duplicated in the Meta.fields list. Remove the duplicate entry to keep the serializer definition clean.

Suggested change
"email_type",
"email_type",
"email_type",

"sender_name",
"sender_email",
"theme_colors",
"footer_text",
"header_image",
"show_logo",
"social_links",
"metadata",
]
extra_kwargs = {
"id": {"read_only": True},
"email_type": {"required": True},
"header_image": {"write_only": True},
"brand_logo_dark": {"read_only": True},
"brand_logo_light": {"read_only": True},
"brand_name": {"read_only": True},
}

def to_representation(self, instance):
data = super().to_representation(instance)
if instance.header_image:
data["url_header_image"] = get_presigned_url(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.header_image}",
)
return data

image_mapping = {
EmailTypes.INVITATION_TO_SPACE: "auth/invitation.png",
EmailTypes.VERIFICATION_CODE: "auth/verification_code.png",
EmailTypes.RESET_PASSWORD: "auth/forget_password.png",
}
image_path = image_mapping.get(instance.email_type, "")
host = settings.HOST.rstrip("/")
data["url_header_image"] = (
f"{host}/static/images/{image_path}" if image_path else ""
)
return data

def get_brand_logo_dark(self, instance):
return get_theme_logo_url(instance, "dark")

def get_brand_logo_light(self, instance):
return get_theme_logo_url(instance, "light")

def get_brand_name(self, instance):
organization = getattr(instance, "organization", None)
setting = (
getattr(organization, "organization_settings", None)
if organization
else None
)
return getattr(setting, "brand_name", "") or ""
Loading
Loading