diff --git a/apps/custom_page/__init__.py b/apps/custom_page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/custom_page/apps.py b/apps/custom_page/apps.py new file mode 100644 index 0000000..a5a381b --- /dev/null +++ b/apps/custom_page/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CustomPageConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.custom_page" diff --git a/apps/custom_page/constants.py b/apps/custom_page/constants.py new file mode 100644 index 0000000..91027fe --- /dev/null +++ b/apps/custom_page/constants.py @@ -0,0 +1,8 @@ +from django.db import models + + +class PageTypes(models.TextChoices): + SIGN_IN = "sign_in" + SIGN_UP = "sign_up" + FORGET_PASSWORD = "forget_password" # nosec B105 + CHANGE_PASSWORD = "change_password" # nosec B105 diff --git a/apps/custom_page/migrations/0001_initial.py b/apps/custom_page/migrations/0001_initial.py new file mode 100644 index 0000000..9a5840b --- /dev/null +++ b/apps/custom_page/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 5.0.6 on 2026-05-27 09:20 + +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="CustomPage", + 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)), + ( + "page_type", + models.CharField( + choices=[ + ("sign_in", "Sign In"), + ("sign_up", "Sign Up"), + ("forget_password", "Forget Password"), + ("change_password", "Change Password"), + ], + max_length=255, + ), + ), + ("title", models.CharField(max_length=255)), + ("subtitle", models.CharField(blank=True, max_length=255, null=True)), + ("metadata", models.JSONField(blank=True, default=dict)), + ("theme_colors", models.JSONField(blank=True, default=dict)), + ( + "background_image", + models.CharField(blank=True, default="", max_length=500), + ), + ("show_logo", models.BooleanField(default=True)), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="organization_custom_page", + to="organization.organization", + ), + ), + ], + options={ + "db_table": "custom_pages", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/apps/custom_page/migrations/0002_backfill_default_custom_pages.py b/apps/custom_page/migrations/0002_backfill_default_custom_pages.py new file mode 100644 index 0000000..75143c1 --- /dev/null +++ b/apps/custom_page/migrations/0002_backfill_default_custom_pages.py @@ -0,0 +1,50 @@ +from django.db import migrations + +from apps.custom_page.service import get_default_pages + + +def backfill_default_custom_pages(apps, schema_editor): + CustomPage = apps.get_model("custom_page", "CustomPage") + Organization = apps.get_model("organization", "Organization") + + pages_to_create = [] + existing_pages = {} + for org_id, page_type in CustomPage.objects.values_list( + "organization_id", "page_type" + ): + if org_id not in existing_pages: + existing_pages[org_id] = set() + existing_pages[org_id].add(page_type) + + for organization in Organization.objects.all().iterator(): + existing_page_types = existing_pages.get(organization.id, set()) + + for page in get_default_pages(): + if page["page_type"] in existing_page_types: + continue + + pages_to_create.append( + CustomPage( + organization_id=organization.id, + page_type=page["page_type"], + title=page["title"], + subtitle=page["subtitle"], + metadata=page["metadata"], + theme_colors=page["theme_colors"], + background_image=page["background_image"], + show_logo=page["show_logo"], + ) + ) + + if pages_to_create: + CustomPage.objects.bulk_create(pages_to_create, batch_size=50) + + +class Migration(migrations.Migration): + dependencies = [ + ("custom_page", "0001_initial"), + ] + + operations = [ + migrations.RunPython(backfill_default_custom_pages, migrations.RunPython.noop), + ] diff --git a/apps/custom_page/migrations/__init__.py b/apps/custom_page/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/custom_page/models.py b/apps/custom_page/models.py new file mode 100644 index 0000000..b5717f2 --- /dev/null +++ b/apps/custom_page/models.py @@ -0,0 +1,24 @@ +from common.models.base_model import BaseModel +from django.db import models + +from apps.custom_page.constants import PageTypes +from apps.organization.models import Organization + + +class CustomPage(BaseModel): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name="organization_custom_page", + ) + page_type = models.CharField(max_length=255, choices=PageTypes.choices) + title = models.CharField(max_length=255) + subtitle = models.CharField(max_length=255, null=True, blank=True) + metadata = models.JSONField(default=dict, blank=True) + theme_colors = models.JSONField(default=dict, blank=True) + background_image = models.CharField(max_length=500, blank=True, default="") + show_logo = models.BooleanField(default=True) + + class Meta: + db_table = "custom_pages" + ordering = ["-created_at"] diff --git a/apps/custom_page/serializers.py b/apps/custom_page/serializers.py new file mode 100644 index 0000000..cfe181d --- /dev/null +++ b/apps/custom_page/serializers.py @@ -0,0 +1,40 @@ +from common.apps.upload_file.service import get_presigned_url +from django.conf import settings +from rest_framework import serializers + +from apps.custom_page.models import CustomPage + + +class CustomPageSerializer(serializers.ModelSerializer): + class Meta: + model = CustomPage + fields = [ + "id", + "page_type", + "title", + "subtitle", + "metadata", + "theme_colors", + "background_image", + "show_logo", + "created_at", + "updated_at", + ] + extra_kwargs = { + "id": {"read_only": True}, + "background_image": {"write_only": True}, + "created_at": {"read_only": True}, + "updated_at": {"read_only": True}, + } + + def validate_background_image(self, value): + return value or "" + + def to_representation(self, instance): + data = super().to_representation(instance) + if instance.background_image: + data["url_background_image"] = get_presigned_url( + settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"), + f"uploads/{instance.background_image}", + ) + return data diff --git a/apps/custom_page/service.py b/apps/custom_page/service.py new file mode 100644 index 0000000..74cacca --- /dev/null +++ b/apps/custom_page/service.py @@ -0,0 +1,65 @@ +from apps.custom_page.constants import PageTypes +from apps.custom_page.models import CustomPage + + +def get_default_pages(): + return [ + { + "page_type": PageTypes.SIGN_IN, + "title": "Sign in to SpaceDF", + "subtitle": "Sign in to manage your IoT devices", + "metadata": {}, + "theme_colors": {}, + "background_image": "", + "show_logo": True, + }, + { + "page_type": PageTypes.SIGN_UP, + "title": "Sign up to SpaceDF", + "subtitle": "Sign up to manage your IoT devices", + "metadata": {}, + "theme_colors": {}, + "background_image": "", + "show_logo": True, + }, + { + "page_type": PageTypes.FORGET_PASSWORD, + "title": "Forgot Your Password?", + "subtitle": "Enter your email address and we will send you instructions to reset your password.", + "metadata": {}, + "theme_colors": { + "background_color": "#FFFFFF", + }, + "background_image": "", + "show_logo": True, + }, + { + "page_type": PageTypes.CHANGE_PASSWORD, + "title": "Create New Password", + "subtitle": "Create your new password. If you forget it, then you have to do forget password", + "metadata": {}, + "theme_colors": { + "background_color": "#FFFFFF", + }, + "background_image": "", + "show_logo": True, + }, + ] + + +def create_default_pages(organization): + default_pages = get_default_pages() + list_data = [ + CustomPage( + organization=organization, + page_type=page.get("page_type"), + title=page.get("title"), + subtitle=page.get("subtitle"), + metadata=page.get("metadata"), + theme_colors=page.get("theme_colors"), + background_image=page.get("background_image"), + show_logo=page.get("show_logo"), + ) + for page in default_pages + ] + CustomPage.objects.bulk_create(list_data) diff --git a/apps/custom_page/urls.py b/apps/custom_page/urls.py new file mode 100644 index 0000000..966b7cb --- /dev/null +++ b/apps/custom_page/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from apps.custom_page.views import ListCustomPageView + +app_name = "custom_page" + +urlpatterns = [ + path("custom-pages", ListCustomPageView.as_view(), name="custom-pages-list"), +] diff --git a/apps/custom_page/views.py b/apps/custom_page/views.py new file mode 100644 index 0000000..faea208 --- /dev/null +++ b/apps/custom_page/views.py @@ -0,0 +1,16 @@ +from common.pagination.base_pagination import BasePagination +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import OrderingFilter + +from apps.custom_page.models import CustomPage +from apps.custom_page.serializers import CustomPageSerializer +from utils.views import OrganizationListAPIView + + +class ListCustomPageView(OrganizationListAPIView): + serializer_class = CustomPageSerializer + queryset = CustomPage.objects.select_related("organization").all() + organization_field = "organization" + pagination_class = BasePagination + filter_backends = [DjangoFilterBackend, OrderingFilter] + ordering = ["-created_at"] diff --git a/bootstrap_service/management/commands/init_organization.py b/bootstrap_service/management/commands/init_organization.py index da8eff0..c3f2b9c 100644 --- a/bootstrap_service/management/commands/init_organization.py +++ b/bootstrap_service/management/commands/init_organization.py @@ -24,6 +24,7 @@ from django.utils import timezone from apps.authentication.models import RootUser +from apps.custom_page.service import create_default_pages from apps.organization.models import Organization from apps.organization_roles.constants import OrganizationRoleType from apps.organization_roles.models import OrganizationRoleUser @@ -117,6 +118,7 @@ def _create_organization_with_roles(self, org_id, org_name, org_slug, result): self.stdout.write(self.style.SUCCESS(f"Created organization: {org_name}")) create_default_policies(organization) + create_default_pages(organization) self.stdout.write(self.style.SUCCESS("Created default policies")) role_mappings = [ diff --git a/bootstrap_service/settings.py b/bootstrap_service/settings.py index 0770998..738897c 100644 --- a/bootstrap_service/settings.py +++ b/bootstrap_service/settings.py @@ -60,6 +60,7 @@ "common.apps.refresh_tokens", "bootstrap_service", "apps.organization", + "apps.custom_page", "apps.organization_roles", "apps.authentication", ] diff --git a/bootstrap_service/urls.py b/bootstrap_service/urls.py index 1eca210..b09fad2 100644 --- a/bootstrap_service/urls.py +++ b/bootstrap_service/urls.py @@ -54,5 +54,6 @@ def health_check(_): path("bootstrap/admin/", admin.site.urls), # apis path("api/bootstrap/", include("apps.authentication.urls")), + path("api/", include("apps.custom_page.urls")), path("api/", include("apps.organization.urls")), ]