761 lines
22 KiB
Python
761 lines
22 KiB
Python
import re
|
||
|
||
from django.contrib.auth import authenticate
|
||
from django.contrib.auth.hashers import check_password
|
||
from django.contrib.auth.tokens import default_token_generator
|
||
from django.utils.http import urlsafe_base64_decode
|
||
from drf_spectacular.utils import extend_schema_field
|
||
from rest_framework import serializers
|
||
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken
|
||
|
||
from common.utils import CURRENCY_SYMBOLS
|
||
from common.models import (
|
||
Activity,
|
||
Address,
|
||
APISettings,
|
||
Attachments,
|
||
Comment,
|
||
Document,
|
||
Org,
|
||
Profile,
|
||
Tags,
|
||
Teams,
|
||
User,
|
||
)
|
||
|
||
|
||
class OrgAwareRefreshToken(RefreshToken):
|
||
"""
|
||
Custom RefreshToken that includes org context in the token payload.
|
||
|
||
This ensures the org context is cryptographically signed and cannot be
|
||
forged by the client. The middleware should validate org_id from this
|
||
token instead of trusting the org header.
|
||
|
||
Embedded claims (to avoid extra API calls):
|
||
- org_id: Organization UUID
|
||
- org_name: Organization name (for display)
|
||
- role: User's role in the org (ADMIN/USER)
|
||
- org_settings: Currency and locale settings
|
||
"""
|
||
|
||
@classmethod
|
||
def for_user_and_org(cls, user, org, profile=None):
|
||
"""
|
||
Generate a refresh token for a user with org context.
|
||
|
||
Args:
|
||
user: User instance
|
||
org: Org instance or org_id UUID
|
||
profile: Optional Profile instance for role
|
||
|
||
Returns:
|
||
OrgAwareRefreshToken with org claims
|
||
"""
|
||
token = cls.for_user(user)
|
||
|
||
# Add org context to the token payload
|
||
if org:
|
||
org_id = str(org.id) if hasattr(org, "id") else str(org)
|
||
token["org_id"] = org_id
|
||
# Add org_name for display (avoids /api/auth/profile call)
|
||
if hasattr(org, "name"):
|
||
token["org_name"] = org.name
|
||
# Add org settings for currency/locale
|
||
if hasattr(org, "default_currency"):
|
||
token["org_settings"] = {
|
||
"default_currency": org.default_currency or "USD",
|
||
"currency_symbol": CURRENCY_SYMBOLS.get(
|
||
org.default_currency or "USD", "$"
|
||
),
|
||
"default_country": org.default_country,
|
||
}
|
||
|
||
# Add role if profile provided (avoids /api/auth/profile call)
|
||
if profile:
|
||
token["role"] = profile.role
|
||
|
||
return token
|
||
|
||
|
||
class OrganizationSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Org
|
||
fields = ("id", "name", "api_key")
|
||
|
||
|
||
class OrgSettingsSerializer(serializers.ModelSerializer):
|
||
"""Serializer for org settings (currency, country, locale, company profile)"""
|
||
|
||
currency_symbol = serializers.SerializerMethodField()
|
||
logo_url = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Org
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
# Company profile
|
||
"company_name",
|
||
"logo",
|
||
"logo_url",
|
||
"address_line",
|
||
"city",
|
||
"state",
|
||
"postcode",
|
||
"country",
|
||
"phone",
|
||
"email",
|
||
"website",
|
||
"tax_id",
|
||
# Locale settings
|
||
"default_currency",
|
||
"default_country",
|
||
"currency_symbol",
|
||
]
|
||
read_only_fields = ["id", "currency_symbol", "logo_url"]
|
||
|
||
@extend_schema_field(str)
|
||
def get_currency_symbol(self, obj):
|
||
return CURRENCY_SYMBOLS.get(obj.default_currency or "USD", "$")
|
||
|
||
@extend_schema_field(str)
|
||
def get_logo_url(self, obj):
|
||
if obj.logo:
|
||
request = self.context.get("request")
|
||
if request:
|
||
return request.build_absolute_uri(obj.logo.url)
|
||
return obj.logo.url
|
||
return None
|
||
|
||
|
||
class TagsSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Tags
|
||
fields = (
|
||
"id",
|
||
"name",
|
||
"slug",
|
||
"color",
|
||
"description",
|
||
"is_active",
|
||
"created_at",
|
||
)
|
||
read_only_fields = ("id", "slug", "created_at")
|
||
|
||
|
||
class SocialLoginSerializer(serializers.Serializer):
|
||
token = serializers.CharField()
|
||
|
||
|
||
class CommentSerializer(serializers.ModelSerializer):
|
||
"""Serializer for Comment model using ContentType"""
|
||
|
||
content_type = serializers.SlugRelatedField(slug_field="model", read_only=True)
|
||
|
||
class Meta:
|
||
model = Comment
|
||
fields = (
|
||
"id",
|
||
"comment",
|
||
"commented_on",
|
||
"commented_by",
|
||
"content_type",
|
||
"object_id",
|
||
"org",
|
||
)
|
||
|
||
|
||
class CommentCreateSerializer(serializers.ModelSerializer):
|
||
"""Serializer for creating comments with ContentType"""
|
||
|
||
content_type = serializers.CharField(write_only=True)
|
||
object_id = serializers.UUIDField(write_only=True)
|
||
|
||
class Meta:
|
||
model = Comment
|
||
fields = (
|
||
"comment",
|
||
"content_type",
|
||
"object_id",
|
||
)
|
||
|
||
def create(self, validated_data):
|
||
from django.contrib.contenttypes.models import ContentType
|
||
|
||
content_type_str = validated_data.pop("content_type")
|
||
try:
|
||
content_type = ContentType.objects.get(model=content_type_str.lower())
|
||
except ContentType.DoesNotExist:
|
||
raise serializers.ValidationError(
|
||
f"Invalid content type: {content_type_str}"
|
||
)
|
||
|
||
validated_data["content_type"] = content_type
|
||
return super().create(validated_data)
|
||
|
||
|
||
class LeadCommentSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Comment
|
||
fields = (
|
||
"id",
|
||
"comment",
|
||
"commented_on",
|
||
"commented_by",
|
||
)
|
||
|
||
|
||
class OrgProfileCreateSerializer(serializers.ModelSerializer):
|
||
"""
|
||
It is for creating organization
|
||
"""
|
||
|
||
name = serializers.CharField(max_length=255)
|
||
|
||
class Meta:
|
||
model = Org
|
||
fields = ["name"]
|
||
extra_kwargs = {"name": {"required": True}}
|
||
|
||
def validate_name(self, name):
|
||
if bool(re.search(r"[~\!@#\$%\^&\*\(\)\+{}\":;'/\[\]]", name)):
|
||
raise serializers.ValidationError(
|
||
"organization name should not contain any special characters"
|
||
)
|
||
if Org.objects.filter(name=name).exists():
|
||
raise serializers.ValidationError(
|
||
"Organization already exists with this name"
|
||
)
|
||
return name
|
||
|
||
|
||
class ShowOrganizationListSerializer(serializers.ModelSerializer):
|
||
"""
|
||
we are using it for show orjanization list
|
||
"""
|
||
|
||
org = OrganizationSerializer()
|
||
|
||
class Meta:
|
||
model = Profile
|
||
fields = (
|
||
"role",
|
||
"alternate_phone",
|
||
"has_sales_access",
|
||
"has_marketing_access",
|
||
"is_organization_admin",
|
||
"org",
|
||
)
|
||
|
||
|
||
class BillingAddressSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Address
|
||
fields = ("address_line", "street", "city", "state", "postcode", "country")
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
account_view = kwargs.pop("account", False)
|
||
|
||
super().__init__(*args, **kwargs)
|
||
|
||
if account_view:
|
||
self.fields["address_line"].required = True
|
||
self.fields["street"].required = True
|
||
self.fields["city"].required = True
|
||
self.fields["state"].required = True
|
||
self.fields["postcode"].required = True
|
||
self.fields["country"].required = True
|
||
|
||
|
||
class CreateUserSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = User
|
||
fields = (
|
||
"email",
|
||
"profile_pic",
|
||
)
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.org = kwargs.pop("org", None)
|
||
super().__init__(*args, **kwargs)
|
||
self.fields["email"].required = True
|
||
|
||
def validate_email(self, email):
|
||
if self.instance:
|
||
if self.instance.email != email:
|
||
if not Profile.objects.filter(user__email=email, org=self.org).exists():
|
||
return email
|
||
raise serializers.ValidationError("Email already exists")
|
||
return email
|
||
if not Profile.objects.filter(user__email=email.lower(), org=self.org).exists():
|
||
return email
|
||
raise serializers.ValidationError("Given Email id already exists")
|
||
|
||
|
||
class CreateProfileSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Profile
|
||
fields = (
|
||
"role",
|
||
"phone",
|
||
"alternate_phone",
|
||
"has_sales_access",
|
||
"has_marketing_access",
|
||
"is_organization_admin",
|
||
)
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields["alternate_phone"].required = False
|
||
self.fields["role"].required = True
|
||
self.fields["phone"].required = True
|
||
|
||
|
||
class UserSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = User
|
||
fields = ["id", "email", "profile_pic"]
|
||
|
||
|
||
class ProfileSerializer(serializers.ModelSerializer):
|
||
# address = BillingAddressSerializer()
|
||
user_details = serializers.SerializerMethodField()
|
||
|
||
@extend_schema_field(dict)
|
||
def get_user_details(self, obj):
|
||
return obj.user_details
|
||
|
||
class Meta:
|
||
model = Profile
|
||
fields = (
|
||
"id",
|
||
"user_details",
|
||
"role",
|
||
"address",
|
||
"has_marketing_access",
|
||
"has_sales_access",
|
||
"phone",
|
||
"date_of_joining",
|
||
"is_active",
|
||
"created_at",
|
||
)
|
||
|
||
|
||
class AttachmentsSerializer(serializers.ModelSerializer):
|
||
"""Serializer for Attachments model using ContentType"""
|
||
|
||
file_path = serializers.SerializerMethodField()
|
||
content_type = serializers.SlugRelatedField(slug_field="model", read_only=True)
|
||
|
||
@extend_schema_field(str)
|
||
def get_file_path(self, obj):
|
||
if obj.attachment:
|
||
return obj.attachment.url
|
||
return None
|
||
|
||
class Meta:
|
||
model = Attachments
|
||
fields = [
|
||
"id",
|
||
"created_by",
|
||
"file_name",
|
||
"created_at",
|
||
"file_path",
|
||
"content_type",
|
||
"object_id",
|
||
"org",
|
||
]
|
||
|
||
|
||
class AttachmentsCreateSerializer(serializers.ModelSerializer):
|
||
"""Serializer for creating attachments with ContentType"""
|
||
|
||
content_type = serializers.CharField(write_only=True)
|
||
object_id = serializers.UUIDField(write_only=True)
|
||
|
||
class Meta:
|
||
model = Attachments
|
||
fields = (
|
||
"file_name",
|
||
"attachment",
|
||
"content_type",
|
||
"object_id",
|
||
)
|
||
|
||
def create(self, validated_data):
|
||
from django.contrib.contenttypes.models import ContentType
|
||
|
||
content_type_str = validated_data.pop("content_type")
|
||
try:
|
||
content_type = ContentType.objects.get(model=content_type_str.lower())
|
||
except ContentType.DoesNotExist:
|
||
raise serializers.ValidationError(
|
||
f"Invalid content type: {content_type_str}"
|
||
)
|
||
|
||
validated_data["content_type"] = content_type
|
||
return super().create(validated_data)
|
||
|
||
|
||
class DocumentSerializer(serializers.ModelSerializer):
|
||
shared_to = ProfileSerializer(read_only=True, many=True)
|
||
teams = serializers.SerializerMethodField()
|
||
created_by = UserSerializer()
|
||
org = OrganizationSerializer()
|
||
|
||
@extend_schema_field(list)
|
||
def get_teams(self, obj):
|
||
return obj.teams.all().values()
|
||
|
||
class Meta:
|
||
model = Document
|
||
fields = [
|
||
"id",
|
||
"title",
|
||
"document_file",
|
||
"status",
|
||
"shared_to",
|
||
"teams",
|
||
"created_at",
|
||
"created_by",
|
||
"org",
|
||
]
|
||
|
||
|
||
class DocumentCreateSerializer(serializers.ModelSerializer):
|
||
def __init__(self, *args, **kwargs):
|
||
request_obj = kwargs.pop("request_obj", None)
|
||
super().__init__(*args, **kwargs)
|
||
self.fields["title"].required = True
|
||
self.org = request_obj.profile.org
|
||
|
||
def validate_title(self, title):
|
||
if self.instance:
|
||
if (
|
||
Document.objects.filter(title__iexact=title, org=self.org)
|
||
.exclude(id=self.instance.id)
|
||
.exists()
|
||
):
|
||
raise serializers.ValidationError(
|
||
"Document with this Title already exists"
|
||
)
|
||
if Document.objects.filter(title__iexact=title, org=self.org).exists():
|
||
raise serializers.ValidationError("Document with this Title already exists")
|
||
return title
|
||
|
||
class Meta:
|
||
model = Document
|
||
fields = ["title", "document_file", "status", "org"]
|
||
|
||
|
||
def find_urls(string):
|
||
# website_regex = "^((http|https)://)?([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com or google.com
|
||
# website_regex = "^https?://([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com
|
||
# http(s)://google.com
|
||
website_regex = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$"
|
||
# http(s)://google.com:8000
|
||
website_regex_port = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}:[0-9]{2,4}$"
|
||
url = re.findall(website_regex, string)
|
||
url_port = re.findall(website_regex_port, string)
|
||
if url and url[0] != "":
|
||
return url
|
||
return url_port
|
||
|
||
|
||
class APISettingsSerializer(serializers.ModelSerializer):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
class Meta:
|
||
model = APISettings
|
||
fields = ("title", "website")
|
||
|
||
def validate_website(self, website):
|
||
if website and not (
|
||
website.startswith("http://") or website.startswith("https://")
|
||
):
|
||
raise serializers.ValidationError("Please provide valid schema")
|
||
if not len(find_urls(website)) > 0:
|
||
raise serializers.ValidationError(
|
||
"Please provide a valid URL with schema and without trailing slash - Example: http://google.com"
|
||
)
|
||
return website
|
||
|
||
|
||
class APISettingsListSerializer(serializers.ModelSerializer):
|
||
created_by = UserSerializer()
|
||
lead_assigned_to = ProfileSerializer(read_only=True, many=True)
|
||
tags = serializers.SerializerMethodField()
|
||
org = OrganizationSerializer()
|
||
|
||
@extend_schema_field(list)
|
||
def get_tags(self, obj):
|
||
return obj.tags.all().values()
|
||
|
||
class Meta:
|
||
model = APISettings
|
||
fields = [
|
||
"title",
|
||
"apikey",
|
||
"website",
|
||
"created_at",
|
||
"created_by",
|
||
"lead_assigned_to",
|
||
"tags",
|
||
"org",
|
||
]
|
||
|
||
|
||
class APISettingsSwaggerSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = APISettings
|
||
fields = [
|
||
"title",
|
||
"website",
|
||
"lead_assigned_to",
|
||
"tags",
|
||
]
|
||
|
||
|
||
class DocumentCreateSwaggerSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Document
|
||
fields = [
|
||
"title",
|
||
"document_file",
|
||
"teams",
|
||
"shared_to",
|
||
]
|
||
|
||
|
||
class DocumentEditSwaggerSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Document
|
||
fields = ["title", "document_file", "teams", "shared_to", "status"]
|
||
|
||
|
||
class UserCreateSwaggerSerializer(serializers.Serializer):
|
||
"""
|
||
It is swagger for creating or updating user
|
||
"""
|
||
|
||
ROLE_CHOICES = ["ADMIN", "USER"]
|
||
|
||
email = serializers.CharField(max_length=1000, required=True)
|
||
role = serializers.ChoiceField(choices=ROLE_CHOICES, required=True)
|
||
phone = serializers.CharField(max_length=12)
|
||
alternate_phone = serializers.CharField(max_length=12)
|
||
address_line = serializers.CharField(max_length=10000, required=True)
|
||
street = serializers.CharField(max_length=1000)
|
||
city = serializers.CharField(max_length=1000)
|
||
state = serializers.CharField(max_length=1000)
|
||
pincode = serializers.CharField(max_length=1000)
|
||
country = serializers.CharField(max_length=1000)
|
||
|
||
|
||
class UserUpdateStatusSwaggerSerializer(serializers.Serializer):
|
||
STATUS_CHOICES = ["Active", "Inactive"]
|
||
|
||
status = serializers.ChoiceField(choices=STATUS_CHOICES, required=True)
|
||
|
||
|
||
# JWT Authentication Serializers for SvelteKit Integration
|
||
|
||
|
||
class LoginSerializer(serializers.Serializer):
|
||
"""Serializer for user login with email and password"""
|
||
|
||
email = serializers.EmailField(required=True)
|
||
password = serializers.CharField(required=True, write_only=True)
|
||
|
||
def validate(self, attrs):
|
||
email = attrs.get("email")
|
||
password = attrs.get("password")
|
||
|
||
if email and password:
|
||
user = authenticate(username=email, password=password)
|
||
if not user:
|
||
raise serializers.ValidationError("Invalid email or password")
|
||
if not user.is_active:
|
||
raise serializers.ValidationError("User account is disabled")
|
||
else:
|
||
raise serializers.ValidationError('Must include "email" and "password"')
|
||
|
||
attrs["user"] = user
|
||
return attrs
|
||
|
||
|
||
class RegisterSerializer(serializers.ModelSerializer):
|
||
"""Serializer for user registration"""
|
||
|
||
password = serializers.CharField(write_only=True, required=True, min_length=8)
|
||
confirm_password = serializers.CharField(write_only=True, required=True)
|
||
|
||
class Meta:
|
||
model = User
|
||
fields = ["email", "password", "confirm_password"]
|
||
|
||
def validate(self, attrs):
|
||
if attrs["password"] != attrs["confirm_password"]:
|
||
raise serializers.ValidationError(
|
||
{"password": "Password fields didn't match."}
|
||
)
|
||
return attrs
|
||
|
||
def create(self, validated_data):
|
||
validated_data.pop("confirm_password")
|
||
user = User.objects.create_user(
|
||
email=validated_data["email"],
|
||
password=validated_data["password"],
|
||
is_active=False, # User needs to activate account
|
||
)
|
||
return user
|
||
|
||
|
||
class UserDetailSerializer(serializers.ModelSerializer):
|
||
"""Detailed user serializer with profile and organizations"""
|
||
|
||
organizations = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = User
|
||
fields = ["id", "email", "profile_pic", "is_active", "organizations"]
|
||
|
||
@extend_schema_field(list)
|
||
def get_organizations(self, obj):
|
||
"""Get all organizations the user belongs to"""
|
||
profiles = Profile.objects.filter(user=obj, is_active=True)
|
||
return [
|
||
{
|
||
"id": str(profile.org.id),
|
||
"name": profile.org.name,
|
||
"role": profile.role,
|
||
"is_organization_admin": profile.is_organization_admin,
|
||
"has_sales_access": profile.has_sales_access,
|
||
"has_marketing_access": profile.has_marketing_access,
|
||
}
|
||
for profile in profiles
|
||
]
|
||
|
||
|
||
class ProfileDetailSerializer(serializers.ModelSerializer):
|
||
"""Detailed profile serializer for authenticated user"""
|
||
|
||
user = UserSerializer(read_only=True)
|
||
org = OrganizationSerializer(read_only=True)
|
||
|
||
class Meta:
|
||
model = Profile
|
||
fields = [
|
||
"id",
|
||
"user",
|
||
"org",
|
||
"role",
|
||
"is_organization_admin",
|
||
"has_sales_access",
|
||
"has_marketing_access",
|
||
"phone",
|
||
"date_of_joining",
|
||
"is_active",
|
||
]
|
||
|
||
|
||
# Activity Serializers for Dashboard Recent Activities
|
||
|
||
|
||
class ActivityUserSerializer(serializers.Serializer):
|
||
"""Simplified user info for activity display"""
|
||
|
||
id = serializers.UUIDField(source="user.id")
|
||
email = serializers.EmailField(source="user.email")
|
||
name = serializers.SerializerMethodField()
|
||
profile_pic = serializers.CharField(source="user.profile_pic", allow_null=True)
|
||
|
||
@extend_schema_field(str)
|
||
def get_name(self, obj):
|
||
"""Get display name from email"""
|
||
return obj.user.email.split("@")[0]
|
||
|
||
|
||
class ActivitySerializer(serializers.ModelSerializer):
|
||
"""Serializer for recent activities"""
|
||
|
||
user = ActivityUserSerializer(read_only=True)
|
||
action_display = serializers.CharField(source="get_action_display", read_only=True)
|
||
timestamp = serializers.DateTimeField(source="created_at", read_only=True)
|
||
humanized_time = serializers.CharField(source="created_on_arrow", read_only=True)
|
||
|
||
class Meta:
|
||
model = Activity
|
||
fields = [
|
||
"id",
|
||
"user",
|
||
"action",
|
||
"action_display",
|
||
"entity_type",
|
||
"entity_id",
|
||
"entity_name",
|
||
"description",
|
||
"timestamp",
|
||
"humanized_time",
|
||
]
|
||
|
||
|
||
class TeamsSerializer(serializers.ModelSerializer):
|
||
users = ProfileSerializer(read_only=True, many=True)
|
||
created_by = UserSerializer()
|
||
|
||
class Meta:
|
||
model = Teams
|
||
fields = (
|
||
"id",
|
||
"name",
|
||
"description",
|
||
"users",
|
||
"created_at",
|
||
"created_by",
|
||
)
|
||
|
||
|
||
class TeamCreateSerializer(serializers.ModelSerializer):
|
||
def __init__(self, *args, **kwargs):
|
||
request_obj = kwargs.pop("request_obj", None)
|
||
super().__init__(*args, **kwargs)
|
||
self.org = request_obj.profile.org
|
||
|
||
self.fields["name"].required = True
|
||
self.fields["description"].required = False
|
||
|
||
def validate_name(self, name):
|
||
if self.instance:
|
||
if (
|
||
Teams.objects.filter(name__iexact=name, org=self.org)
|
||
.exclude(id=self.instance.id)
|
||
.exists()
|
||
):
|
||
raise serializers.ValidationError("Team already exists with this name")
|
||
else:
|
||
if Teams.objects.filter(name__iexact=name).exists():
|
||
raise serializers.ValidationError("Team already exists with this name")
|
||
return name
|
||
|
||
class Meta:
|
||
model = Teams
|
||
fields = (
|
||
"name",
|
||
"description",
|
||
"created_at",
|
||
"created_by",
|
||
"org",
|
||
)
|
||
|
||
|
||
class TeamswaggerCreateSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Teams
|
||
fields = (
|
||
"name",
|
||
"description",
|
||
"users",
|
||
)
|