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", )