From 4707f8b5f9c0e0fd38d8e585215222649f80909d Mon Sep 17 00:00:00 2001
From: ali-zipstack <muhammad.ali@zipstack.com>
Date: Mon, 1 Jul 2024 22:50:08 +0530
Subject: [PATCH 1/3] version 2 of user/organization tenant account module -
 UN-1424

---
 backend/tenant_account_v2/__init__.py         |   0
 backend/tenant_account_v2/admin.py            |   1 +
 backend/tenant_account_v2/apps.py             |   6 +
 backend/tenant_account_v2/constants.py        |  14 ++
 backend/tenant_account_v2/dto.py              |  15 ++
 backend/tenant_account_v2/enums.py            |   6 +
 backend/tenant_account_v2/invitation_urls.py  |  20 ++
 backend/tenant_account_v2/invitation_views.py |  46 ++++
 .../migrations/0001_initial.py                |  70 +++++++
 ...rganizationmember_organization_and_more.py |  37 ++++
 .../tenant_account_v2/migrations/__init__.py  |   0
 backend/tenant_account_v2/models.py           |  48 +++++
 .../organization_member_service.py            | 144 +++++++++++++
 backend/tenant_account_v2/serializer.py       | 159 ++++++++++++++
 backend/tenant_account_v2/templates/land.html |  11 +
 backend/tenant_account_v2/tests.py            |   1 +
 backend/tenant_account_v2/urls.py             |  11 +
 backend/tenant_account_v2/users_urls.py       |  37 ++++
 backend/tenant_account_v2/users_view.py       | 198 ++++++++++++++++++
 backend/tenant_account_v2/views.py            |  89 ++++++++
 20 files changed, 913 insertions(+)
 create mode 100644 backend/tenant_account_v2/__init__.py
 create mode 100644 backend/tenant_account_v2/admin.py
 create mode 100644 backend/tenant_account_v2/apps.py
 create mode 100644 backend/tenant_account_v2/constants.py
 create mode 100644 backend/tenant_account_v2/dto.py
 create mode 100644 backend/tenant_account_v2/enums.py
 create mode 100644 backend/tenant_account_v2/invitation_urls.py
 create mode 100644 backend/tenant_account_v2/invitation_views.py
 create mode 100644 backend/tenant_account_v2/migrations/0001_initial.py
 create mode 100644 backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
 create mode 100644 backend/tenant_account_v2/migrations/__init__.py
 create mode 100644 backend/tenant_account_v2/models.py
 create mode 100644 backend/tenant_account_v2/organization_member_service.py
 create mode 100644 backend/tenant_account_v2/serializer.py
 create mode 100644 backend/tenant_account_v2/templates/land.html
 create mode 100644 backend/tenant_account_v2/tests.py
 create mode 100644 backend/tenant_account_v2/urls.py
 create mode 100644 backend/tenant_account_v2/users_urls.py
 create mode 100644 backend/tenant_account_v2/users_view.py
 create mode 100644 backend/tenant_account_v2/views.py

diff --git a/backend/tenant_account_v2/__init__.py b/backend/tenant_account_v2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/tenant_account_v2/admin.py b/backend/tenant_account_v2/admin.py
new file mode 100644
index 0000000000..846f6b4061
--- /dev/null
+++ b/backend/tenant_account_v2/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/backend/tenant_account_v2/apps.py b/backend/tenant_account_v2/apps.py
new file mode 100644
index 0000000000..cd128f028b
--- /dev/null
+++ b/backend/tenant_account_v2/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TenantAccountV2Config(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "tenant_account_v2"
diff --git a/backend/tenant_account_v2/constants.py b/backend/tenant_account_v2/constants.py
new file mode 100644
index 0000000000..29ba63684f
--- /dev/null
+++ b/backend/tenant_account_v2/constants.py
@@ -0,0 +1,14 @@
+class PlatformServiceConstants:
+    IS_ACTIVE = "is_active"
+    KEY = "key"
+    ORGANIZATION = "organization"
+    ID = "id"
+    ACTIVATE = "ACTIVATE"
+    DEACTIVATE = "DEACTIVATE"
+    ACTION = "action"
+    KEY_NAME = "key_name"
+
+
+class ErrorMessage:
+    KEY_EXIST = "Key name already exists"
+    DUPLICATE_API = "It appears that a duplicate call may have been made."
diff --git a/backend/tenant_account_v2/dto.py b/backend/tenant_account_v2/dto.py
new file mode 100644
index 0000000000..16c37ea583
--- /dev/null
+++ b/backend/tenant_account_v2/dto.py
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class OrganizationLoginResponse:
+    name: str
+    display_name: str
+    organization_id: str
+    created_at: str
+
+
+@dataclass
+class ResetUserPasswordDto:
+    status: bool
+    message: str
diff --git a/backend/tenant_account_v2/enums.py b/backend/tenant_account_v2/enums.py
new file mode 100644
index 0000000000..d8209ec2d3
--- /dev/null
+++ b/backend/tenant_account_v2/enums.py
@@ -0,0 +1,6 @@
+from enum import Enum
+
+
+class UserRole(Enum):
+    USER = "user"
+    ADMIN = "admin"
diff --git a/backend/tenant_account_v2/invitation_urls.py b/backend/tenant_account_v2/invitation_urls.py
new file mode 100644
index 0000000000..aed1f37a1d
--- /dev/null
+++ b/backend/tenant_account_v2/invitation_urls.py
@@ -0,0 +1,20 @@
+from django.urls import path
+from tenant_account_v2.invitation_views import InvitationViewSet
+
+invitation_list = InvitationViewSet.as_view(
+    {
+        "get": InvitationViewSet.list_invitations.__name__,
+    }
+)
+
+invitation_details = InvitationViewSet.as_view(
+    {
+        "delete": InvitationViewSet.delete_invitation.__name__,
+    }
+)
+
+
+urlpatterns = [
+    path("", invitation_list, name="invitation_list"),
+    path("<str:id>/", invitation_details, name="invitation_details"),
+]
diff --git a/backend/tenant_account_v2/invitation_views.py b/backend/tenant_account_v2/invitation_views.py
new file mode 100644
index 0000000000..adbd726955
--- /dev/null
+++ b/backend/tenant_account_v2/invitation_views.py
@@ -0,0 +1,46 @@
+import logging
+
+from account_v2.authentication_controller import AuthenticationController
+from account_v2.dto import MemberInvitation
+from rest_framework import status, viewsets
+from rest_framework.decorators import action
+from rest_framework.request import Request
+from rest_framework.response import Response
+from tenant_account_v2.serializer import ListInvitationsResponseSerializer
+from utils.user_session import UserSessionUtils
+
+Logger = logging.getLogger(__name__)
+
+
+class InvitationViewSet(viewsets.ViewSet):
+    @action(detail=False, methods=["GET"])
+    def list_invitations(self, request: Request) -> Response:
+        auth_controller = AuthenticationController()
+        invitations: list[MemberInvitation] = auth_controller.get_user_invitations(
+            organization_id=UserSessionUtils.get_organization_id(request),
+        )
+        serialized_members = ListInvitationsResponseSerializer(
+            invitations, many=True
+        ).data
+        return Response(
+            status=status.HTTP_200_OK,
+            data={"message": "success", "members": serialized_members},
+        )
+
+    @action(detail=False, methods=["DELETE"])
+    def delete_invitation(self, request: Request, id: str) -> Response:
+        auth_controller = AuthenticationController()
+        is_deleted: bool = auth_controller.delete_user_invitation(
+            organization_id=UserSessionUtils.get_organization_id(request),
+            invitation_id=id,
+        )
+        if is_deleted:
+            return Response(
+                status=status.HTTP_204_NO_CONTENT,
+                data={"status": "success", "message": "success"},
+            )
+        else:
+            return Response(
+                status=status.HTTP_404_NOT_FOUND,
+                data={"status": "failed", "message": "failed"},
+            )
diff --git a/backend/tenant_account_v2/migrations/0001_initial.py b/backend/tenant_account_v2/migrations/0001_initial.py
new file mode 100644
index 0000000000..d32120713f
--- /dev/null
+++ b/backend/tenant_account_v2/migrations/0001_initial.py
@@ -0,0 +1,70 @@
+# Generated by Django 4.2.1 on 2024-06-25 11:56
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("account", "0007_organization_allowed_token_limit"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="OrganizationMember",
+            fields=[
+                ("member_id", models.BigAutoField(primary_key=True, serialize=False)),
+                ("role", models.CharField()),
+                (
+                    "is_login_onboarding_msg",
+                    models.BooleanField(
+                        db_comment="Flag to indicate whether the onboarding messages are shown",
+                        default=True,
+                    ),
+                ),
+                (
+                    "is_prompt_studio_onboarding_msg",
+                    models.BooleanField(
+                        db_comment="Flag to indicate whether the prompt studio messages are shown",
+                        default=True,
+                    ),
+                ),
+                (
+                    "organization",
+                    models.ForeignKey(
+                        blank=True,
+                        db_comment="Foreign key reference to the Organization model.",
+                        default=None,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="account.organization",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        default=None,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="organization_user",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Organization Member",
+                "verbose_name_plural": "Organization Members",
+                "db_table": "organization_member_v2",
+            },
+        ),
+        migrations.AddConstraint(
+            model_name="organizationmember",
+            constraint=models.UniqueConstraint(
+                fields=("organization", "user"), name="unique_organization_member"
+            ),
+        ),
+    ]
diff --git a/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py b/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
new file mode 100644
index 0000000000..a436e8d9af
--- /dev/null
+++ b/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.1 on 2024-06-26 02:47
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("account_v2", "0001_initial"),
+        ("tenant_account_v2", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="organizationmember",
+            name="organization",
+            field=models.ForeignKey(
+                blank=True,
+                db_comment="Foreign key reference to the Organization model.",
+                default=None,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                to="account_v2.organization",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="organizationmember",
+            name="user",
+            field=models.ForeignKey(
+                default=None,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="organization_user",
+                to="account_v2.user",
+            ),
+        ),
+    ]
diff --git a/backend/tenant_account_v2/migrations/__init__.py b/backend/tenant_account_v2/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/tenant_account_v2/models.py b/backend/tenant_account_v2/models.py
new file mode 100644
index 0000000000..c2c5b3cdc2
--- /dev/null
+++ b/backend/tenant_account_v2/models.py
@@ -0,0 +1,48 @@
+from account_v2.models import User
+from django.db import models
+from utils.models.organization_mixin import (
+    DefaultOrganizationManagerMixin,
+    DefaultOrganizationMixin,
+)
+
+
+class OrganizationMemberModelManager(DefaultOrganizationManagerMixin, models.Manager):
+    pass
+
+
+class OrganizationMember(DefaultOrganizationMixin):
+    member_id = models.BigAutoField(primary_key=True)
+    user = models.ForeignKey(
+        User,
+        on_delete=models.CASCADE,
+        default=None,
+        related_name="organization_user",
+    )
+    role = models.CharField()
+    is_login_onboarding_msg = models.BooleanField(
+        default=True,
+        db_comment="Flag to indicate whether the onboarding messages are shown",
+    )
+    is_prompt_studio_onboarding_msg = models.BooleanField(
+        default=True,
+        db_comment="Flag to indicate whether the prompt studio messages are shown",
+    )
+
+    def __str__(self):  # type: ignore
+        return (
+            f"OrganizationMember("
+            f"{self.member_id}, role: {self.role}, userId: {self.user.user_id})"
+        )
+
+    objects = OrganizationMemberModelManager()
+
+    class Meta:
+        db_table = "organization_member_v2"
+        verbose_name = "Organization Member"
+        verbose_name_plural = "Organization Members"
+        constraints = [
+            models.UniqueConstraint(
+                fields=["organization", "user"],
+                name="unique_organization_member",
+            ),
+        ]
diff --git a/backend/tenant_account_v2/organization_member_service.py b/backend/tenant_account_v2/organization_member_service.py
new file mode 100644
index 0000000000..ae9dd085cb
--- /dev/null
+++ b/backend/tenant_account_v2/organization_member_service.py
@@ -0,0 +1,144 @@
+from typing import Any, Optional
+
+from tenant_account_v2.models import OrganizationMember
+from utils.cache_service import CacheService
+
+
+class OrganizationMemberService:
+
+    @staticmethod
+    def get_user_by_email(email: str) -> Optional[OrganizationMember]:
+        try:
+            return OrganizationMember.objects.get(user__email=email)  # type: ignore
+        except OrganizationMember.DoesNotExist:
+            return None
+
+    @staticmethod
+    def get_user_by_user_id(user_id: str) -> Optional[OrganizationMember]:
+        try:
+            return OrganizationMember.objects.get(user__user_id=user_id)  # type: ignore
+        except OrganizationMember.DoesNotExist:
+            return None
+
+    @staticmethod
+    def get_user_by_id(id: str) -> Optional[OrganizationMember]:
+        try:
+            return OrganizationMember.objects.get(user=id)  # type: ignore
+        except OrganizationMember.DoesNotExist:
+            return None
+
+    @staticmethod
+    def get_members() -> list[OrganizationMember]:
+        return OrganizationMember.objects.all()
+
+    @staticmethod
+    def get_members_by_user_email(
+        user_emails: list[str], values_list_fields: list[str]
+    ) -> list[dict[str, Any]]:
+        """Get members by user emails.
+
+        Parameters:
+            user_emails (list[str]): The emails of the users to get.
+            values_list_fields (list[str]): The fields to include in the result.
+
+        Returns:
+            list[dict[str, Any]]: The members.
+        """
+        if not user_emails:
+            return []
+        queryset = OrganizationMember.objects.filter(user__email__in=user_emails)
+        if values_list_fields is None:
+            users = queryset.values()
+        else:
+            users = queryset.values_list(*values_list_fields)
+
+        return list(users)
+
+    @staticmethod
+    def delete_user(user: OrganizationMember) -> None:
+        """Delete a user from an organization.
+
+        Parameters:
+            user (OrganizationMember): The user to delete.
+        """
+        user.delete()
+
+    @staticmethod
+    def remove_users_by_user_pks(user_pks: list[str]) -> None:
+        """Remove a users from an organization.
+
+        Parameters:
+            user_pks (list[str]): The primary keys of the users to remove.
+        """
+        OrganizationMember.objects.filter(user__in=user_pks).delete()
+
+    @classmethod
+    def remove_user_by_user_id(cls, user_id: str) -> None:
+        """Remove a user from an organization.
+
+        Parameters:
+            user_id (str): The user_id of the user to remove.
+        """
+        user = cls.get_user_by_user_id(user_id)
+        if user:
+            cls.delete_user(user)
+
+    @staticmethod
+    def get_organization_user_cache_key(user_id: str, organization_id: str) -> str:
+        """Get the cache key for a user in an organization.
+
+        Parameters:
+            organization_id (str): The ID of the organization.
+
+        Returns:
+            str: The cache key for a user in the organization.
+        """
+        return f"user_organization:{user_id}:{organization_id}"
+
+    @classmethod
+    def check_user_membership_in_organization_cache(
+        cls, user_id: str, organization_id: str
+    ) -> bool:
+        """Check if a user exists in an organization.
+
+        Parameters:
+            user_id (str): The ID of the user to check.
+            organization_id (str): The ID of the organization to check.
+
+        Returns:
+            bool: True if the user exists in the organization, False otherwise.
+        """
+        user_organization_key = cls.get_organization_user_cache_key(
+            user_id, organization_id
+        )
+        return CacheService.check_a_key_exist(user_organization_key)
+
+    @classmethod
+    def set_user_membership_in_organization_cache(
+        cls, user_id: str, organization_id: str
+    ) -> None:
+        """Set a user's membership in an organization in the cache.
+
+        Parameters:
+            user_id (str): The ID of the user.
+            organization_id (str): The ID of the organization.
+        """
+        user_organization_key = cls.get_organization_user_cache_key(
+            user_id, organization_id
+        )
+        CacheService.set_key(user_organization_key, {})
+
+    @classmethod
+    def remove_user_membership_in_organization_cache(
+        cls, user_id: str, organization_id: str
+    ) -> None:
+        """Remove a user's membership in an organization from the cache.
+
+        Parameters:
+            user_id (str): The ID of the user.
+            organization_id (str): The ID of the organization.
+        """
+        user_organization_key = cls.get_organization_user_cache_key(
+            user_id, organization_id
+        )
+        CacheService.delete_a_key(user_organization_key)
diff --git a/backend/tenant_account_v2/serializer.py b/backend/tenant_account_v2/serializer.py
new file mode 100644
index 0000000000..32165ef19f
--- /dev/null
+++ b/backend/tenant_account_v2/serializer.py
@@ -0,0 +1,159 @@
+from collections import OrderedDict
+from typing import Any, Optional, Union, cast
+
+from account_v2.constants import Common
+from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
+from rest_framework.serializers import ModelSerializer
+from tenant_account_v2.models import OrganizationMember
+
+
+class OrganizationCallbackSerializer(serializers.Serializer):
+    id = serializers.CharField(required=False)
+
+
+class OrganizationLoginResponseSerializer(serializers.Serializer):
+    name = serializers.CharField()
+    display_name = serializers.CharField()
+    organization_id = serializers.CharField()
+    created_at = serializers.CharField()
+
+
+class UserInviteResponseSerializer(serializers.Serializer):
+    email = serializers.CharField(required=True)
+    status = serializers.CharField(required=True)
+    message = serializers.CharField(required=False)
+
+
+class OrganizationMemberSerializer(serializers.ModelSerializer):
+    email = serializers.CharField(source="user.email", read_only=True)
+    id = serializers.CharField(source="user.id", read_only=True)
+
+    class Meta:
+        model = OrganizationMember
+        fields = ("id", "email", "role")
+
+
+class LimitedUserEmailListSerializer(serializers.ListSerializer):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        self.max_elements: int = kwargs.pop("max_elements", Common.MAX_EMAIL_IN_REQUEST)
+        super().__init__(*args, **kwargs)
+
+    def validate(self, data: list[str]) -> Any:
+        if len(data) > self.max_elements:
+            raise ValidationError(
+                f"Exceeded maximum number of elements ({self.max_elements})"
+            )
+        return data
+
+
+class LimitedUserListSerializer(serializers.ListSerializer):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        self.max_elements: int = kwargs.pop("max_elements", Common.MAX_EMAIL_IN_REQUEST)
+        super().__init__(*args, **kwargs)
+
+    def validate(
+        self, data: list[dict[str, Union[str, None]]]
+    ) -> list[dict[str, Union[str, None]]]:
+        if len(data) > self.max_elements:
+            raise ValidationError(
+                f"Exceeded maximum number of elements ({self.max_elements})"
+            )
+
+        for item in data:
+            if not isinstance(item, dict):
+                raise ValidationError("Each item in the list must be a dictionary.")
+            if "email" not in item:
+                raise ValidationError("Each item in the list must have 'email' key.")
+            if "role" not in item:
+                item["role"] = None
+
+        return data
+
+
+class InviteUserSerializer(serializers.Serializer):
+    users = LimitedUserListSerializer(
+        required=True,
+        child=serializers.DictField(
+            child=serializers.CharField(max_length=255, required=True),
+            required=False,  # Make 'role' field optional
+        ),
+        max_elements=Common.MAX_EMAIL_IN_REQUEST,
+    )
+
+    def get_users(
+        self, validated_data: dict[str, Any]
+    ) -> list[dict[str, Union[str, None]]]:
+        return validated_data.get("users", [])
+
+
+class RemoveUserFromOrganizationSerializer(serializers.Serializer):
+    emails = LimitedUserEmailListSerializer(
+        required=True,
+        child=serializers.EmailField(required=True),
+        max_elements=Common.MAX_EMAIL_IN_REQUEST,
+    )
+
+    def get_user_emails(
+        self, validated_data: dict[str, Union[list[str], None]]
+    ) -> list[str]:
+        return cast(list[str], validated_data.get(Common.USER_EMAILS, []))
+
+
+class ChangeUserRoleRequestSerializer(serializers.Serializer):
+    email = serializers.EmailField(required=True)
+    role = serializers.CharField(required=True)
+
+    def get_user_email(
+        self, validated_data: dict[str, Union[str, None]]
+    ) -> Optional[str]:
+        return validated_data.get(Common.USER_EMAIL)
+
+    def get_user_role(
+        self, validated_data: dict[str, Union[str, None]]
+    ) -> Optional[str]:
+        return validated_data.get(Common.USER_ROLE)
+
+
+class DeleteInvitationRequestSerializer(serializers.Serializer):
+    id = serializers.EmailField(required=True)
+
+    def get_id(self, validated_data: dict[str, Union[str, None]]) -> Optional[str]:
+        return validated_data.get(Common.ID)
+
+
+class UserInfoSerializer(serializers.Serializer):
+    id = serializers.CharField()
+    email = serializers.CharField()
+    name = serializers.CharField()
+    display_name = serializers.CharField()
+    family_name = serializers.CharField()
+    picture = serializers.CharField()
+
+
+class GetRolesResponseSerializer(serializers.Serializer):
+    id = serializers.CharField()
+    name = serializers.CharField()
+    description = serializers.CharField()
+
+    def to_representation(self, instance: Any) -> OrderedDict[str, Any]:
+        data: OrderedDict[str, Any] = super().to_representation(instance)
+        return data
+
+
+class ListInvitationsResponseSerializer(serializers.Serializer):
+    id = serializers.CharField()
+    email = serializers.CharField()
+    created_at = serializers.CharField()
+    expires_at = serializers.CharField()
+
+    def to_representation(self, instance: Any) -> OrderedDict[str, Any]:
+        data: OrderedDict[str, Any] = super().to_representation(instance)
+        return data
+
+
+class UpdateFlagSerializer(ModelSerializer):
+
+    class Meta:
+        model = OrganizationMember
+        fields = ("is_login_onboarding_msg", "is_prompt_studio_onboarding_msg")
diff --git a/backend/tenant_account_v2/templates/land.html b/backend/tenant_account_v2/templates/land.html
new file mode 100644
index 0000000000..8cec6bca6d
--- /dev/null
+++ b/backend/tenant_account_v2/templates/land.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en" xml:lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>ZipstackID Django App Example</title>
+  </head>
+  <body>
+    <h1 id="profileDropDown">Welcome Guest</h1>
+    <p>dsdasdaddddddddd</p>
+  </body>
+</html>
diff --git a/backend/tenant_account_v2/tests.py b/backend/tenant_account_v2/tests.py
new file mode 100644
index 0000000000..a39b155ac3
--- /dev/null
+++ b/backend/tenant_account_v2/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/backend/tenant_account_v2/urls.py b/backend/tenant_account_v2/urls.py
new file mode 100644
index 0000000000..50e46ab087
--- /dev/null
+++ b/backend/tenant_account_v2/urls.py
@@ -0,0 +1,11 @@
+from django.urls import include, path
+from tenant_account_v2 import invitation_urls, users_urls
+from tenant_account_v2.views import get_organization, get_roles, reset_password
+
+urlpatterns = [
+    path("roles", get_roles, name="roles"),
+    path("users/", include(users_urls)),
+    path("invitation/", include(invitation_urls)),
+    path("organization", get_organization, name="get_organization"),
+    path("reset_password", reset_password, name="reset_password"),
+]
diff --git a/backend/tenant_account_v2/users_urls.py b/backend/tenant_account_v2/users_urls.py
new file mode 100644
index 0000000000..7b2765f3f7
--- /dev/null
+++ b/backend/tenant_account_v2/users_urls.py
@@ -0,0 +1,37 @@
+from django.urls import path
+from tenant_account_v2.users_view import OrganizationUserViewSet
+
+organization_user_role = OrganizationUserViewSet.as_view(
+    {
+        "post": OrganizationUserViewSet.assign_organization_role_to_user.__name__,
+        "delete": OrganizationUserViewSet.remove_organization_role_from_user.__name__,
+    }
+)
+
+user_profile = OrganizationUserViewSet.as_view(
+    {
+        "get": OrganizationUserViewSet.get_user_profile.__name__,
+        "put": OrganizationUserViewSet.update_flags.__name__,
+    }
+)
+
+invite_user = OrganizationUserViewSet.as_view(
+    {
+        "post": OrganizationUserViewSet.invite_user.__name__,
+    }
+)
+
+organization_users = OrganizationUserViewSet.as_view(
+    {
+        "get": OrganizationUserViewSet.get_organization_members.__name__,
+        "delete": OrganizationUserViewSet.remove_members_from_organization.__name__,
+    }
+)
+
+
+urlpatterns = [
+    path("", organization_users, name="organization_user"),
+    path("profile/", user_profile, name="user_profile"),
+    path("role/", organization_user_role, name="organization_user_role"),
+    path("invite/", invite_user, name="invite_user"),
+]
diff --git a/backend/tenant_account_v2/users_view.py b/backend/tenant_account_v2/users_view.py
new file mode 100644
index 0000000000..b9deeea4fa
--- /dev/null
+++ b/backend/tenant_account_v2/users_view.py
@@ -0,0 +1,198 @@
+import logging
+
+from account_v2.authentication_controller import AuthenticationController
+from account_v2.exceptions import BadRequestException
+from rest_framework import status, viewsets
+from rest_framework.decorators import action
+from rest_framework.request import Request
+from rest_framework.response import Response
+from tenant_account_v2.models import OrganizationMember
+from tenant_account_v2.organization_member_service import OrganizationMemberService
+from tenant_account_v2.serializer import (
+    ChangeUserRoleRequestSerializer,
+    InviteUserSerializer,
+    OrganizationMemberSerializer,
+    RemoveUserFromOrganizationSerializer,
+    UpdateFlagSerializer,
+    UserInfoSerializer,
+    UserInviteResponseSerializer,
+)
+from utils.user_session import UserSessionUtils
+
+Logger = logging.getLogger(__name__)
+
+
+class OrganizationUserViewSet(viewsets.ViewSet):
+    @action(detail=False, methods=["POST"])
+    def assign_organization_role_to_user(self, request: Request) -> Response:
+        serializer = ChangeUserRoleRequestSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        user_email = serializer.get_user_email(serializer.validated_data)
+        role = serializer.get_user_role(serializer.validated_data)
+        if not (user_email and role):
+            raise BadRequestException
+        org_id: str = UserSessionUtils.get_organization_id(request)
+        auth_controller = AuthenticationController()
+
+        auth_controller = AuthenticationController()
+        update_status = auth_controller.add_user_role(
+            request.user, org_id, user_email, role
+        )
+        if update_status:
+            return Response(
+                status=status.HTTP_200_OK,
+                data={"status": "success", "message": "success"},
+            )
+        else:
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"status": "failed", "message": "failed"},
+            )
+
+    @action(detail=False, methods=["DELETE"])
+    def remove_organization_role_from_user(self, request: Request) -> Response:
+        serializer = ChangeUserRoleRequestSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        user_email = serializer.get_user_email(serializer.validated_data)
+        role = serializer.get_user_role(serializer.validated_data)
+        if not (user_email and role):
+            raise BadRequestException
+        org_id: str = UserSessionUtils.get_organization_id(request)
+        auth_controller = AuthenticationController()
+
+        auth_controller = AuthenticationController()
+        update_status = auth_controller.remove_user_role(
+            request.user, org_id, user_email, role
+        )
+        if update_status:
+            return Response(
+                status=status.HTTP_200_OK,
+                data={"status": "success", "message": "success"},
+            )
+        else:
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"status": "failed", "message": "failed"},
+            )
+
+    @action(detail=False, methods=["GET"])
+    def get_user_profile(self, request: Request) -> Response:
+        auth_controller = AuthenticationController()
+        try:
+            user_info = auth_controller.get_user_info(request)
+            role = auth_controller.get_organization_members_by_user(request.user)
+            if not user_info:
+                return Response(
+                    status=status.HTTP_404_NOT_FOUND,
+                    data={"message": "User Not Found"},
+                )
+            serialized_user_info = UserInfoSerializer(user_info).data
+            # Temporary fix for getting user role along with user info.
+            # Proper implementation would be adding role field to UserInfo.
+            serialized_user_info["is_admin"] = auth_controller.is_admin_by_role(
+                role.role
+            )
+            # changes for displying onboarding msgs
+            org_member = OrganizationMemberService.get_user_by_id(id=request.user.id)
+            serialized_user_info["login_onboarding_message_displayed"] = (
+                org_member.is_login_onboarding_msg
+            )
+            serialized_user_info["prompt_onboarding_message_displayed"] = (
+                org_member.is_prompt_studio_onboarding_msg
+            )
+
+            return Response(
+                status=status.HTTP_200_OK, data={"user": serialized_user_info}
+            )
+        except Exception as error:
+            Logger.error(f"Error while get User : {error}")
+            return Response(
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+                data={"message": "Internal Error"},
+            )
+
+    @action(detail=False, methods=["POST"])
+    def invite_user(self, request: Request) -> Response:
+        serializer = InviteUserSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        user_list = serializer.get_users(serializer.validated_data)
+        auth_controller = AuthenticationController()
+        invite_response = auth_controller.invite_user(
+            admin=request.user,
+            org_id=UserSessionUtils.get_organization_id(request),
+            user_list=user_list,
+        )
+
+        response_serializer = UserInviteResponseSerializer(invite_response, many=True)
+
+        if invite_response and len(invite_response) != 0:
+            response = Response(
+                status=status.HTTP_200_OK,
+                data={"message": response_serializer.data},
+            )
+        else:
+            response = Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"message": "failed"},
+            )
+        return response
+
+    @action(detail=False, methods=["DELETE"])
+    def remove_members_from_organization(self, request: Request) -> Response:
+        serializer = RemoveUserFromOrganizationSerializer(data=request.data)
+
+        serializer.is_valid(raise_exception=True)
+        user_emails = serializer.get_user_emails(serializer.validated_data)
+        organization_id: str = UserSessionUtils.get_organization_id(request)
+
+        auth_controller = AuthenticationController()
+        is_updated = auth_controller.remove_users_from_organization(
+            admin=request.user,
+            organization_id=organization_id,
+            user_emails=user_emails,
+        )
+        if is_updated:
+            return Response(
+                status=status.HTTP_200_OK,
+                data={"status": "success", "message": "success"},
+            )
+        else:
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"status": "failed", "message": "failed"},
+            )
+
+    @action(detail=False, methods=["GET"])
+    def get_organization_members(self, request: Request) -> Response:
+        auth_controller = AuthenticationController()
+        if UserSessionUtils.get_organization_id(request):
+            members: list[OrganizationMember] = (
+                auth_controller.get_organization_members_by_org_id()
+            )
+            serialized_members = OrganizationMemberSerializer(members, many=True).data
+            return Response(
+                status=status.HTTP_200_OK,
+                data={"message": "success", "members": serialized_members},
+            )
+        return Response(
+            status=status.HTTP_401_UNAUTHORIZED,
+            data={"message": "cookie not found"},
+        )
+
+    @action(detail=False, methods=["PUT"])
+    def update_flags(self, request: Request) -> Response:
+        serializer = UpdateFlagSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        org_member = OrganizationMemberService.get_user_by_id(id=request.user.id)
+        org_member.is_login_onboarding_msg = serializer.validated_data.get(
+            "is_login_onboarding_msg"
+        )
+
+        org_member.is_prompt_studio_onboarding_msg = serializer.validated_data.get(
+            "is_prompt_studio_onboarding_msg"
+        )
+        org_member.save()
+        return Response(
+            status=status.HTTP_200_OK,
+            data={"status": "success", "message": "success"},
+        )
diff --git a/backend/tenant_account_v2/views.py b/backend/tenant_account_v2/views.py
new file mode 100644
index 0000000000..e64cfd4c2e
--- /dev/null
+++ b/backend/tenant_account_v2/views.py
@@ -0,0 +1,89 @@
+import logging
+from typing import Any
+
+from account_v2.authentication_controller import AuthenticationController
+from account_v2.dto import UserRoleData
+from account_v2.models import Organization
+from rest_framework import status
+from rest_framework.decorators import api_view
+from rest_framework.request import Request
+from rest_framework.response import Response
+from tenant_account_v2.dto import OrganizationLoginResponse, ResetUserPasswordDto
+from tenant_account_v2.serializer import (
+    GetRolesResponseSerializer,
+    OrganizationLoginResponseSerializer,
+)
+from utils.user_session import UserSessionUtils
+
+logger = logging.getLogger(__name__)
+
+
+@api_view(["GET"])
+def logout(request: Request) -> Response:
+    auth_controller = AuthenticationController()
+    return auth_controller.user_logout(request)
+
+
+@api_view(["GET"])
+def get_roles(request: Request) -> Response:
+    auth_controller = AuthenticationController()
+    roles: list[UserRoleData] = auth_controller.get_user_roles()
+    serialized_members = GetRolesResponseSerializer(roles, many=True).data
+    return Response(
+        status=status.HTTP_200_OK,
+        data={"message": "success", "members": serialized_members},
+    )
+
+
+@api_view(["POST"])
+def reset_password(request: Request) -> Response:
+    auth_controller = AuthenticationController()
+    data: ResetUserPasswordDto = auth_controller.reset_user_password(request.user)
+    if data.status:
+        return Response(
+            status=status.HTTP_200_OK,
+            data={"status": "success", "message": data.message},
+        )
+    else:
+        return Response(
+            status=status.HTTP_400_BAD_REQUEST,
+            data={"status": "failed", "message": data.message},
+        )
+
+
+@api_view(["GET"])
+def get_organization(request: Request) -> Response:
+    auth_controller = AuthenticationController()
+    try:
+        organization_id = UserSessionUtils.get_organization_id(request)
+        org_data = auth_controller.get_organization_info(organization_id)
+        if not org_data:
+            return Response(
+                status=status.HTTP_404_NOT_FOUND,
+                data={"message": "Org Not Found"},
+            )
+        response = makeSignupResponse(org_data)
+        return Response(
+            status=status.HTTP_201_CREATED,
+            data={"message": "success", "organization": response},
+        )
+
+    except Exception as error:
+        logger.error(f"Error while get User : {error}")
+        return Response(
+            status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            data={"message": "Internal Error"},
+        )
+
+
+def makeSignupResponse(
+    organization: Organization,
+) -> Any:
+    return OrganizationLoginResponseSerializer(
+        OrganizationLoginResponse(
+            organization.name,
+            organization.display_name,
+            organization.organization_id,
+            organization.created_at,
+        )
+    ).data

From 6c48fa84ccddf216e24a93d3d2438fba9bec6948 Mon Sep 17 00:00:00 2001
From: ali-zipstack <muhammad.ali@zipstack.com>
Date: Thu, 11 Jul 2024 12:29:04 +0530
Subject: [PATCH 2/3] removed migrations, Will be created once after merge

---
 .../migrations/0001_initial.py                | 70 -------------------
 ...rganizationmember_organization_and_more.py | 37 ----------
 .../tenant_account_v2/migrations/__init__.py  |  0
 3 files changed, 107 deletions(-)
 delete mode 100644 backend/tenant_account_v2/migrations/0001_initial.py
 delete mode 100644 backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
 delete mode 100644 backend/tenant_account_v2/migrations/__init__.py

diff --git a/backend/tenant_account_v2/migrations/0001_initial.py b/backend/tenant_account_v2/migrations/0001_initial.py
deleted file mode 100644
index d32120713f..0000000000
--- a/backend/tenant_account_v2/migrations/0001_initial.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Generated by Django 4.2.1 on 2024-06-25 11:56
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-        ("account", "0007_organization_allowed_token_limit"),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name="OrganizationMember",
-            fields=[
-                ("member_id", models.BigAutoField(primary_key=True, serialize=False)),
-                ("role", models.CharField()),
-                (
-                    "is_login_onboarding_msg",
-                    models.BooleanField(
-                        db_comment="Flag to indicate whether the onboarding messages are shown",
-                        default=True,
-                    ),
-                ),
-                (
-                    "is_prompt_studio_onboarding_msg",
-                    models.BooleanField(
-                        db_comment="Flag to indicate whether the prompt studio messages are shown",
-                        default=True,
-                    ),
-                ),
-                (
-                    "organization",
-                    models.ForeignKey(
-                        blank=True,
-                        db_comment="Foreign key reference to the Organization model.",
-                        default=None,
-                        null=True,
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="account.organization",
-                    ),
-                ),
-                (
-                    "user",
-                    models.ForeignKey(
-                        default=None,
-                        on_delete=django.db.models.deletion.CASCADE,
-                        related_name="organization_user",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "verbose_name": "Organization Member",
-                "verbose_name_plural": "Organization Members",
-                "db_table": "organization_member_v2",
-            },
-        ),
-        migrations.AddConstraint(
-            model_name="organizationmember",
-            constraint=models.UniqueConstraint(
-                fields=("organization", "user"), name="unique_organization_member"
-            ),
-        ),
-    ]
diff --git a/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py b/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
deleted file mode 100644
index a436e8d9af..0000000000
--- a/backend/tenant_account_v2/migrations/0002_alter_organizationmember_organization_and_more.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Generated by Django 4.2.1 on 2024-06-26 02:47
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ("account_v2", "0001_initial"),
-        ("tenant_account_v2", "0001_initial"),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name="organizationmember",
-            name="organization",
-            field=models.ForeignKey(
-                blank=True,
-                db_comment="Foreign key reference to the Organization model.",
-                default=None,
-                null=True,
-                on_delete=django.db.models.deletion.CASCADE,
-                to="account_v2.organization",
-            ),
-        ),
-        migrations.AlterField(
-            model_name="organizationmember",
-            name="user",
-            field=models.ForeignKey(
-                default=None,
-                on_delete=django.db.models.deletion.CASCADE,
-                related_name="organization_user",
-                to="account_v2.user",
-            ),
-        ),
-    ]
diff --git a/backend/tenant_account_v2/migrations/__init__.py b/backend/tenant_account_v2/migrations/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000

From 025305facb766e5525628bf4dd36cea6d136f180 Mon Sep 17 00:00:00 2001
From: ali-zipstack <muhammad.ali@zipstack.com>
Date: Fri, 12 Jul 2024 17:26:02 +0530
Subject: [PATCH 3/3] removed unwanted templates

---
 backend/tenant_account_v2/templates/land.html | 11 -----------
 1 file changed, 11 deletions(-)
 delete mode 100644 backend/tenant_account_v2/templates/land.html

diff --git a/backend/tenant_account_v2/templates/land.html b/backend/tenant_account_v2/templates/land.html
deleted file mode 100644
index 8cec6bca6d..0000000000
--- a/backend/tenant_account_v2/templates/land.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" xml:lang="en">
-  <head>
-    <meta charset="utf-8" />
-    <title>ZipstackID Django App Example</title>
-  </head>
-  <body>
-    <h1 id="profileDropDown">Welcome Guest</h1>
-    <p>dsdasdaddddddddd</p>
-  </body>
-</html>
