Merge branch 'refs/heads/main' into storefront
This commit is contained in:
commit
9220de5e63
16 changed files with 191 additions and 61 deletions
|
|
@ -20,6 +20,7 @@ from core.viewsets import (
|
|||
CategoryViewSet,
|
||||
FeedbackViewSet,
|
||||
OrderViewSet,
|
||||
ProductTagViewSet,
|
||||
ProductViewSet,
|
||||
PromoCodeViewSet,
|
||||
PromotionViewSet,
|
||||
|
|
@ -41,6 +42,7 @@ core_router.register(r"stocks", StockViewSet, basename="stocks")
|
|||
core_router.register(r"promo_codes", PromoCodeViewSet, basename="promo_codes")
|
||||
core_router.register(r"promotions", PromotionViewSet, basename="promotions")
|
||||
core_router.register(r"addresses", AddressViewSet, basename="addresses")
|
||||
core_router.register(r"product_tags", ProductTagViewSet, basename="product_tags")
|
||||
|
||||
sitemaps = {
|
||||
"products": ProductSitemap,
|
||||
|
|
|
|||
BIN
core/docs/images/evibes-big-simple.png
Normal file
BIN
core/docs/images/evibes-big-simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.db.models import Avg, FloatField, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
|
|
@ -251,7 +252,7 @@ class WishlistFilter(FilterSet):
|
|||
class CategoryFilter(FilterSet):
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
||||
name = CharFilter(field_name="name", lookup_expr="icontains")
|
||||
parent_uuid = UUIDFilter(field_name="parent__uuid", lookup_expr="exact")
|
||||
parent_uuid = CharFilter(method="filter_parent_uuid")
|
||||
slug = CharFilter(field_name="slug", lookup_expr="exact")
|
||||
|
||||
order_by = OrderingFilter(
|
||||
|
|
@ -264,7 +265,22 @@ class CategoryFilter(FilterSet):
|
|||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ["uuid", "name"]
|
||||
fields = ["uuid", "name", "parent_uuid", "slug"]
|
||||
|
||||
def filter_parent_uuid(self, queryset, name, value):
|
||||
"""
|
||||
If ?parent_uuid= or ?parent_uuid=null, return items with parent=None.
|
||||
Otherwise treat `value` as a real UUID and filter parent__uuid=value.
|
||||
"""
|
||||
if value in ("", "null", "None"):
|
||||
return queryset.filter(parent=None)
|
||||
|
||||
try:
|
||||
uuid_val = uuid.UUID(value)
|
||||
except (ValueError, TypeError):
|
||||
return queryset
|
||||
|
||||
return queryset.filter(parent__uuid=uuid_val)
|
||||
|
||||
|
||||
class BrandFilter(FilterSet):
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from core.models import (
|
|||
OrderProduct,
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductTag,
|
||||
PromoCode,
|
||||
Promotion,
|
||||
Stock,
|
||||
|
|
@ -140,7 +141,7 @@ class CategoryType(DjangoObjectType):
|
|||
if depth <= 0:
|
||||
return Category.objects.none()
|
||||
|
||||
categories = Category.objects.language(info.context.locale).filter(parent=self)
|
||||
categories = Category.objects.filter(parent=self)
|
||||
if info.context.user.has_perm("core.view_category"):
|
||||
return categories
|
||||
return categories.filter(is_active=True)
|
||||
|
|
@ -455,6 +456,17 @@ class WishlistType(DjangoObjectType):
|
|||
description = _("wishlists")
|
||||
|
||||
|
||||
class ProductTagType(DjangoObjectType):
|
||||
product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products"))
|
||||
|
||||
class Meta:
|
||||
model = ProductTag
|
||||
interfaces = (relay.Node,)
|
||||
fields = ("uuid", "tag_name", "name", "product_set")
|
||||
filter_fields = ["uuid", "tag_name", "name"]
|
||||
description = _("product tags")
|
||||
|
||||
|
||||
class ConfigType(ObjectType):
|
||||
project_name = String(description=_("project name"))
|
||||
base_domain = String(description=_("company email"))
|
||||
|
|
@ -511,5 +523,5 @@ class SearchResultsType(ObjectType):
|
|||
|
||||
|
||||
class BulkActionOrderProductInput(InputObjectType):
|
||||
id = UUID(required=True)
|
||||
uuid = UUID(required=True)
|
||||
attributes = GenericScalar(required=False)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from core.graphene.object_types import (
|
|||
OrderProductType,
|
||||
OrderType,
|
||||
ProductImageType,
|
||||
ProductTagType,
|
||||
ProductType,
|
||||
PromoCodeType,
|
||||
PromotionType,
|
||||
|
|
@ -64,6 +65,7 @@ from core.models import (
|
|||
OrderProduct,
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductTag,
|
||||
PromoCode,
|
||||
Promotion,
|
||||
Stock,
|
||||
|
|
@ -108,6 +110,7 @@ class Query(ObjectType):
|
|||
product_images = DjangoFilterConnectionField(ProductImageType)
|
||||
stocks = DjangoFilterConnectionField(StockType)
|
||||
wishlists = DjangoFilterConnectionField(WishlistType, filterset_class=WishlistFilter)
|
||||
product_tags = DjangoFilterConnectionField(ProductTagType)
|
||||
promotions = DjangoFilterConnectionField(PromotionType)
|
||||
promocodes = DjangoFilterConnectionField(PromoCodeType)
|
||||
brands = DjangoFilterConnectionField(BrandType, filterset_class=BrandFilter)
|
||||
|
|
@ -184,7 +187,7 @@ class Query(ObjectType):
|
|||
|
||||
@staticmethod
|
||||
def resolve_categories(_parent, info, **kwargs):
|
||||
categories = Category.objects.filter(parent=None)
|
||||
categories = Category.objects.all()
|
||||
if info.context.user.has_perm("core.view_category"):
|
||||
return categories
|
||||
return categories.filter(is_active=True)
|
||||
|
|
@ -280,6 +283,12 @@ class Query(ObjectType):
|
|||
return promocodes.filter(user__uuid=kwargs.get("user_uuid")) or promocodes.all()
|
||||
return promocodes.filter(is_active=True, user=info.context.user)
|
||||
|
||||
@staticmethod
|
||||
def resolve_product_tags(_parent, info, **kwargs):
|
||||
if info.context.user.has_perm("core.view_producttag"):
|
||||
return ProductTag.objects.all()
|
||||
return ProductTag.objects.filter(is_active=True)
|
||||
|
||||
|
||||
class Mutation(ObjectType):
|
||||
search = Search.Field()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class AddressManager(models.Manager):
|
|||
"addressdetails": 1,
|
||||
"q": raw_data,
|
||||
}
|
||||
resp = requests.get(config.NOMINATIM_URL, params=params)
|
||||
resp = requests.get(config.NOMINATIM_URL.rstrip("/") + "/search", params=params)
|
||||
resp.raise_for_status()
|
||||
results = resp.json()
|
||||
if not results:
|
||||
|
|
@ -31,7 +31,7 @@ class AddressManager(models.Manager):
|
|||
|
||||
# Parse address components
|
||||
addr = data.get("address", {})
|
||||
street = addr.get("road") or addr.get("pedestrian") or ""
|
||||
street = f"{addr.get('road', '') or addr.get('pedestrian', '')}, {addr.get('house_number', '')}"
|
||||
district = addr.get("city_district") or addr.get("suburb") or ""
|
||||
city = addr.get("city") or addr.get("town") or addr.get("village") or ""
|
||||
region = addr.get("state") or addr.get("region") or ""
|
||||
|
|
@ -49,6 +49,7 @@ class AddressManager(models.Manager):
|
|||
# Create the model instance, storing both the input string and full API response
|
||||
return super().create(
|
||||
raw_data=raw_data,
|
||||
address_line=f"{kwargs.get('address_line_1')}, {kwargs.get('address_line_2')}",
|
||||
street=street,
|
||||
district=district,
|
||||
city=city,
|
||||
|
|
|
|||
18
core/migrations/0023_address_address_line.py
Normal file
18
core/migrations/0023_address_address_line.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2 on 2025-05-28 19:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('core', '0022_category_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='address',
|
||||
name='address_line',
|
||||
field=models.TextField(blank=True, help_text='address line for the customer', null=True,
|
||||
verbose_name='address line'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1259,6 +1259,12 @@ class Documentary(NiceModel):
|
|||
class Address(NiceModel):
|
||||
is_publicly_visible = False
|
||||
|
||||
address_line = TextField( # noqa: DJ001
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("address line for the customer"),
|
||||
verbose_name=_("address line"),
|
||||
)
|
||||
street = CharField(_("street"), max_length=255, null=True) # noqa: DJ001
|
||||
district = CharField(_("district"), max_length=255, null=True) # noqa: DJ001
|
||||
city = CharField(_("city"), max_length=100, null=True) # noqa: DJ001
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class EvibesPermission(permissions.BasePermission):
|
|||
- Standard model perms ('add', 'view', 'change', 'delete') are enforced for all other actions,
|
||||
including for staff users.
|
||||
- Publicly visible models allow anonymous list/retrieve.
|
||||
- If an instance or queryset has a "user" attribute, ensure that the request.user is the same,
|
||||
unless the user is an admin with the required django permission.
|
||||
"""
|
||||
|
||||
ACTION_PERM_MAP = {
|
||||
|
|
@ -34,6 +36,8 @@ class EvibesPermission(permissions.BasePermission):
|
|||
}
|
||||
|
||||
USER_SCOPED_ACTIONS = {
|
||||
"list",
|
||||
"retrieve",
|
||||
"buy",
|
||||
"buy_unregistered",
|
||||
"current",
|
||||
|
|
@ -64,19 +68,56 @@ class EvibesPermission(permissions.BasePermission):
|
|||
if request.user.has_perm(f"{app_label}.{codename}"):
|
||||
return True
|
||||
|
||||
return bool(action in ("list", "retrieve") and getattr(model, "is_publicly_visible", False))
|
||||
return bool(
|
||||
action in ("list", "retrieve")
|
||||
and getattr(model, "is_publicly_visible", False)
|
||||
)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
if hasattr(obj, "user"):
|
||||
if obj.user == request.user:
|
||||
return True
|
||||
# Allow admins who hold the required model permission
|
||||
app_label = obj._meta.app_label
|
||||
model_name = obj._meta.model_name
|
||||
action = getattr(view, "action", None)
|
||||
perm_prefix = self.ACTION_PERM_MAP.get(action)
|
||||
return bool(perm_prefix and request.user.has_perm(f"{app_label}.{perm_prefix}_{model_name}"))
|
||||
|
||||
model = view.queryset.model
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
action = getattr(view, "action", None)
|
||||
perm_prefix = self.ACTION_PERM_MAP.get(action)
|
||||
return bool(perm_prefix and request.user.has_perm(f"{app_label}.{perm_prefix}_{model_name}"))
|
||||
|
||||
def has_queryset_permission(self, request, view, queryset):
|
||||
"""
|
||||
Filter the base queryset according to the action and user.
|
||||
Staff users still require view permissions to see records.
|
||||
For models with a "user" field, restrict access to records belonging to the request user
|
||||
unless the admin holds the needed permissions.
|
||||
"""
|
||||
model = view.queryset.model
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
if view.action in self.USER_SCOPED_ACTIONS:
|
||||
return queryset.filter(user=request.user)
|
||||
if hasattr(model, "user"):
|
||||
if view.action in self.USER_SCOPED_ACTIONS:
|
||||
return queryset.filter(user=request.user)
|
||||
if view.action in ("list", "retrieve"):
|
||||
if request.user.has_perm(f"{app_label}.view_{model_name}"):
|
||||
return queryset
|
||||
return queryset.none()
|
||||
|
||||
base = queryset.filter(is_active=True, user=request.user)
|
||||
if request.user.is_staff and request.user.has_perm(
|
||||
f"{app_label}.{self.ACTION_PERM_MAP.get(view.action)}_{model_name}"
|
||||
):
|
||||
return queryset.filter(is_active=True)
|
||||
return base
|
||||
|
||||
if view.action in ("list", "retrieve"):
|
||||
if request.user.has_perm(f"{app_label}.view_{model_name}"):
|
||||
|
|
@ -87,10 +128,7 @@ class EvibesPermission(permissions.BasePermission):
|
|||
|
||||
base = queryset.filter(is_active=True)
|
||||
match view.action:
|
||||
case "update":
|
||||
if request.user.has_perm(f"{app_label}.change_{model_name}"):
|
||||
return base
|
||||
case "partial_update":
|
||||
case "update" | "partial_update":
|
||||
if request.user.has_perm(f"{app_label}.change_{model_name}"):
|
||||
return base
|
||||
case "destroy":
|
||||
|
|
|
|||
|
|
@ -158,10 +158,24 @@ class AddressCreateSerializer(ModelSerializer): # noqa: F405
|
|||
write_only=True,
|
||||
max_length=512,
|
||||
)
|
||||
address_line_1 = CharField(
|
||||
write_only=True,
|
||||
max_length=128,
|
||||
required=False
|
||||
)
|
||||
address_line_2 = CharField(
|
||||
write_only=True,
|
||||
max_length=128,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Address
|
||||
fields = ["raw_data"]
|
||||
fields = [
|
||||
"raw_data",
|
||||
"address_line_1",
|
||||
"address_line_2"
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
raw = validated_data.pop("raw_data")
|
||||
|
|
|
|||
|
|
@ -89,20 +89,19 @@ class CategoryDetailSerializer(ModelSerializer):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
distinct_vals_list = list(distinct_vals)
|
||||
distinct_vals_list = list(distinct_vals)[0:128] if len(list(distinct_vals)) > 128 else list(distinct_vals)
|
||||
|
||||
if len(distinct_vals_list) <= 256:
|
||||
filterable_results.append(
|
||||
{
|
||||
"attribute_name": attr.name,
|
||||
"possible_values": distinct_vals_list,
|
||||
"value_type": attr.value_type,
|
||||
}
|
||||
)
|
||||
else:
|
||||
continue
|
||||
filterable_results.append(
|
||||
{
|
||||
"attribute_name": attr.name,
|
||||
"possible_values": distinct_vals_list,
|
||||
"value_type": attr.value_type,
|
||||
}
|
||||
)
|
||||
|
||||
if not user.has_perm("view_attribute"):
|
||||
cache.set(f"{obj.uuid}_filterable_results", filterable_results, 86400)
|
||||
|
||||
cache.set(f"{obj.uuid}_filterable_results", filterable_results, 86400)
|
||||
return filterable_results
|
||||
|
||||
def get_children(self, obj) -> list[dict]:
|
||||
|
|
@ -123,7 +122,7 @@ class CategoryDetailSerializer(ModelSerializer):
|
|||
|
||||
|
||||
class BrandDetailSerializer(ModelSerializer):
|
||||
categories = CategoryDetailSerializer(many=True)
|
||||
categories = CategorySimpleSerializer(many=True)
|
||||
small_logo = SerializerMethodField()
|
||||
big_logo = SerializerMethodField()
|
||||
|
||||
|
|
|
|||
|
|
@ -73,13 +73,18 @@ from core.serializers import (
|
|||
OrderProductSimpleSerializer,
|
||||
OrderSimpleSerializer,
|
||||
ProductDetailSerializer,
|
||||
ProductImageDetailSerializer,
|
||||
ProductImageSimpleSerializer,
|
||||
ProductSimpleSerializer,
|
||||
ProductTagDetailSerializer,
|
||||
ProductTagSimpleSerializer,
|
||||
PromoCodeDetailSerializer,
|
||||
PromoCodeSimpleSerializer,
|
||||
PromotionDetailSerializer,
|
||||
PromotionSimpleSerializer,
|
||||
RemoveOrderProductSerializer,
|
||||
RemoveWishlistProductSerializer,
|
||||
StockDetailSerializer,
|
||||
StockSimpleSerializer,
|
||||
VendorSimpleSerializer,
|
||||
WishlistDetailSerializer,
|
||||
|
|
@ -364,21 +369,11 @@ class OrderProductViewSet(EvibesViewSet):
|
|||
}
|
||||
|
||||
|
||||
class ProductTagViewSet(EvibesViewSet):
|
||||
queryset = ProductTag.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["tag_name", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": ProductTagSimpleSerializer,
|
||||
}
|
||||
|
||||
|
||||
class ProductImageViewSet(EvibesViewSet):
|
||||
queryset = ProductImage.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["product", "priority", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
serializer_class = ProductImageDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": ProductImageSimpleSerializer,
|
||||
}
|
||||
|
|
@ -388,7 +383,7 @@ class PromoCodeViewSet(EvibesViewSet):
|
|||
queryset = PromoCode.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["code", "discount_amount", "discount_percent", "start_time", "end_time", "used_on", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
serializer_class = PromoCodeDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": PromoCodeSimpleSerializer,
|
||||
}
|
||||
|
|
@ -398,7 +393,7 @@ class PromotionViewSet(EvibesViewSet):
|
|||
queryset = Promotion.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["name", "discount_percent", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
serializer_class = PromotionDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": PromotionSimpleSerializer,
|
||||
}
|
||||
|
|
@ -408,7 +403,7 @@ class StockViewSet(EvibesViewSet):
|
|||
queryset = Stock.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["vendor", "product", "sku", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
serializer_class = StockDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": StockSimpleSerializer,
|
||||
}
|
||||
|
|
@ -419,7 +414,7 @@ class WishlistViewSet(EvibesViewSet):
|
|||
queryset = Wishlist.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["user", "is_active"]
|
||||
serializer_class = AttributeGroupDetailSerializer
|
||||
serializer_class = WishlistDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": WishlistSimpleSerializer,
|
||||
}
|
||||
|
|
@ -535,3 +530,13 @@ class AddressViewSet(EvibesViewSet):
|
|||
)
|
||||
|
||||
return Response(suggestions, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProductTagViewSet(EvibesViewSet):
|
||||
queryset = ProductTag.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["tag_name", "is_active"]
|
||||
serializer_class = ProductTagDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": ProductTagSimpleSerializer,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import logging
|
|||
from os import getenv
|
||||
from pathlib import Path
|
||||
|
||||
EVIBES_VERSION = "2.7.0"
|
||||
EVIBES_VERSION = "2.7.1"
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "eVibes"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
description = "eVibes is an open-source eCommerce backend service built with Django. It’s designed for flexibility, making it ideal for various use cases and learning Django skills. The project is easy to customize, allowing for straightforward editing and extension."
|
||||
authors = ["fureunoir <contact@fureunoir.com>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from hmac import compare_digest
|
|||
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core.exceptions import BadRequest, PermissionDenied
|
||||
from django.core.exceptions import BadRequest, PermissionDenied, ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.http import Http404
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
|
|
@ -11,7 +11,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
from graphene import UUID, Boolean, Field, List, String
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_file_upload.scalars import Upload
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from core.graphene import BaseMutation
|
||||
from core.utils.messages import permission_denied_message
|
||||
|
|
@ -123,8 +122,8 @@ class UpdateUser(BaseMutation):
|
|||
password = kwargs.get("password", "")
|
||||
confirm_password = kwargs.get("confirm_password", "")
|
||||
|
||||
if compare_digest(password.lower(), email.lower()):
|
||||
raise BadRequest(_("password too weak"))
|
||||
if password:
|
||||
validate_password(password=password, user=user)
|
||||
|
||||
if not compare_digest(password, "") and compare_digest(password, confirm_password):
|
||||
user.set_password(password)
|
||||
|
|
@ -314,13 +313,15 @@ class ConfirmResetPassword(BaseMutation):
|
|||
if not password_reset_token.check_token(user, token):
|
||||
raise BadRequest(_("token is invalid!"))
|
||||
|
||||
validate_password(password=password, user=user)
|
||||
|
||||
user.set_password(password)
|
||||
|
||||
user.save()
|
||||
|
||||
return ConfirmResetPassword(success=True)
|
||||
|
||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e:
|
||||
except (TypeError, ValueError, OverflowError, ValidationError, User.DoesNotExist) as e:
|
||||
raise BadRequest(_(f"something went wrong: {e!s}"))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import traceback
|
|||
from contextlib import suppress
|
||||
from secrets import compare_digest
|
||||
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -20,7 +22,6 @@ from evibes.settings import DEBUG
|
|||
from vibes_auth.docs.drf.viewsets import USER_SCHEMA
|
||||
from vibes_auth.models import User
|
||||
from vibes_auth.serializers import (
|
||||
ConfirmPasswordResetSerializer,
|
||||
UserSerializer,
|
||||
)
|
||||
from vibes_auth.utils.emailing import send_reset_password_email_task
|
||||
|
|
@ -64,29 +65,34 @@ class UserViewSet(
|
|||
|
||||
@action(detail=False, methods=["post"])
|
||||
@method_decorator(ratelimit(key="ip", rate="2/h" if not DEBUG else "888/h"))
|
||||
def confirm_password_reset(self):
|
||||
def confirm_password_reset(self, request, *args, **kwargs):
|
||||
try:
|
||||
data = ConfirmPasswordResetSerializer(self.request.data).data
|
||||
|
||||
if not compare_digest(data.get("password"), data.get("confirm_password")):
|
||||
if not compare_digest(request.data.get("password"), request.data.get("confirm_password")):
|
||||
return Response(
|
||||
{"error": _("passwords do not match")},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
uuid = urlsafe_base64_decode(data.get("uidb64")).decode()
|
||||
uuid = urlsafe_base64_decode(request.data.get("uidb64")).decode()
|
||||
user = User.objects.get(pk=uuid)
|
||||
|
||||
validate_password(password=request.data.get("password"), user=user)
|
||||
|
||||
password_reset_token = PasswordResetTokenGenerator()
|
||||
if not password_reset_token.check_token(user, data.get("token")):
|
||||
if not password_reset_token.check_token(user, request.data.get("token")):
|
||||
return Response({"error": _("token is invalid!")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user.set_password(data.get("password"))
|
||||
user.set_password(request.data.get("password"))
|
||||
user.save()
|
||||
return Response({"message": _("password reset successfully")}, status=status.HTTP_200_OK)
|
||||
|
||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except (TypeError, ValueError, OverflowError, ValidationError, User.DoesNotExist) as e:
|
||||
data = {"error": str(e)}
|
||||
if DEBUG:
|
||||
data["detail"] = str(traceback.format_exc())
|
||||
data["received"] = str(request.data)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(ratelimit(key="ip", rate="3/h" if not DEBUG else "888/h"))
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
|
@ -142,6 +148,9 @@ class UserViewSet(
|
|||
return Response(serializer.data)
|
||||
|
||||
def update(self, request, pk=None, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
instance = serializer.update(instance=self.get_object(), validated_data=request.data)
|
||||
return Response(
|
||||
self.get_serializer(self.get_object()).update(instance=self.get_object(), validated_data=request.data).data
|
||||
self.get_serializer(instance.data)
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue