merge
This commit is contained in:
@@ -4,6 +4,7 @@ from djoser.urls.jwt import views as djoser_jwt_views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('profile', views.ProfileView.as_view()),
|
path('profile', views.ProfileView.as_view()),
|
||||||
|
path('verify', djoser_jwt_views.TokenVerifyView.as_view(), name='jwt-verify'),
|
||||||
path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'),
|
path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'),
|
||||||
path('address/create', views.CreateAddressView.as_view(), name='create-address'),
|
path('address/create', views.CreateAddressView.as_view(), name='create-address'),
|
||||||
path('address/edit/<int:pk>', views.EditAddressView.as_view(), name='edit-address'),
|
path('address/edit/<int:pk>', views.EditAddressView.as_view(), name='edit-address'),
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ REST_FRAMEWORK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1),
|
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=2),
|
||||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
|
|
||||||
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||||
# path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
# path('comment/<int:pk>', views.CommentView.as_view(), name='comment-list'),
|
# path('comment/<int:pk>', views.CommentView.as_view(), name='comment-list'),
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-01-14 17:41
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0010_remove_productmodel_link_of_metas'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productmodel',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='زمان ثبت محصول'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-01-14 18:31
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0011_productmodel_created_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productmodel',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.categorymodel'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -112,7 +112,8 @@ class ProductModel(models.Model):
|
|||||||
meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
|
meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
|
||||||
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
|
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
|
||||||
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول')
|
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول')
|
||||||
|
category = models.ForeignKey(CategoryModel, blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
def format_discount_price(self):
|
def format_discount_price(self):
|
||||||
discount_price = int(self.price * (100 - self.discount) / 100)
|
discount_price = int(self.price * (100 - self.discount) / 100)
|
||||||
formatted_num = "{:,.0f}".format(discount_price)
|
formatted_num = "{:,.0f}".format(discount_price)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from .models import *
|
from .models import *
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
class ProductChatSerializer(serializers.ModelSerializer):
|
class ProductChatSerializer(serializers.ModelSerializer):
|
||||||
price = serializers.SerializerMethodField()
|
price = serializers.SerializerMethodField()
|
||||||
|
is_new = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProductModel
|
model = ProductModel
|
||||||
fields = ['name', 'description', 'price', 'in_stock', 'discount', ]
|
fields = ['name', 'description', 'price', 'in_stock', 'discount', ]
|
||||||
@@ -13,11 +16,14 @@ class ProductChatSerializer(serializers.ModelSerializer):
|
|||||||
if dollor_price is None:
|
if dollor_price is None:
|
||||||
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
|
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
|
||||||
if obj.currency == 'toman':
|
if obj.currency == 'toman':
|
||||||
return obj.price
|
toman_price = obj.price
|
||||||
elif obj.currency == 'dollor':
|
elif obj.currency == 'dollor':
|
||||||
return obj.price * dollor_price
|
toman_price = obj.price * dollor_price
|
||||||
elif obj.currency == 'derham':
|
elif obj.currency == 'derham':
|
||||||
return obj.price * dollor_price * dollar_to_dirham
|
toman_price = obj.price * dollor_price * dollar_to_dirham
|
||||||
|
return "{:,.0f} تومان".format(toman_price)
|
||||||
|
def get_is_new(self, obj):
|
||||||
|
return timezone.now() < obj.created_at + timedelta(days=7)
|
||||||
|
|
||||||
class ProductSerializer(ProductChatSerializer):
|
class ProductSerializer(ProductChatSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
+78
-18
@@ -8,28 +8,60 @@ from django.db.models import Q
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||||
from utils.pagination import StructurePagination
|
from utils.pagination import StructurePagination
|
||||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
|
||||||
|
# class CustomAPIView(APIView):
|
||||||
|
# def __init__(self, *args, **kwargs):
|
||||||
|
# super().__init__(*args, **kwargs)
|
||||||
|
# print('here')
|
||||||
|
# print(self.permission_classes)
|
||||||
|
# if not getattr(self, 'permission_classes')[0] != AllowAny or not self.permission_classes:
|
||||||
|
# print('asdf')
|
||||||
|
# self.authentication_classes = []
|
||||||
|
|
||||||
|
|
||||||
class AllCategories(APIView):
|
class AllCategories(APIView):
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
|
authentication_classes = []
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="search",
|
||||||
|
description="Search by category name or description.",
|
||||||
|
required=False,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={
|
||||||
|
200: CategorySerializer(many=True),
|
||||||
|
404: OpenApiTypes.OBJECT,
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
categories = CategoryModel.objects.all()
|
search_query = request.query_params.get('search', None)
|
||||||
|
if search_query:
|
||||||
|
categories = CategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query))
|
||||||
|
else:
|
||||||
|
categories = CategoryModel.objects.all()
|
||||||
categories_ser = self.serializer_class(instance=categories, many=True)
|
categories_ser = self.serializer_class(instance=categories, many=True)
|
||||||
return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK)
|
return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
class ProductView(APIView):
|
class ProductView(APIView):
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = []
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
product = get_object_or_404(ProductModel, id=pk)
|
product = get_object_or_404(ProductModel, id=pk)
|
||||||
product_ser = self.serializer_class(instance=product, many=False)
|
product_ser = self.serializer_class(instance=product, many=False)
|
||||||
return Response(product_ser.data, status=status.HTTP_200_OK)
|
return Response(product_ser.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AllProductsView(APIView):
|
class AllProductsView(APIView):
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
pagination_class = StructurePagination
|
pagination_class = StructurePagination
|
||||||
|
authentication_classes = [] # Add authentication if required
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[
|
parameters=[
|
||||||
@@ -41,10 +73,12 @@ class AllProductsView(APIView):
|
|||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="category",
|
name="category",
|
||||||
description="Filter by category ID.",
|
type={'type': 'array', 'items': {'type': 'number'}},
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
required=False,
|
required=False,
|
||||||
type=OpenApiTypes.INT,
|
style='form',
|
||||||
),
|
explode=False,
|
||||||
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="price_gte",
|
name="price_gte",
|
||||||
description="Filter products with price greater than or equal to this value.",
|
description="Filter products with price greater than or equal to this value.",
|
||||||
@@ -61,7 +95,7 @@ class AllProductsView(APIView):
|
|||||||
name="sort",
|
name="sort",
|
||||||
description=(
|
description=(
|
||||||
"Sort results by one of the following fields:\n"
|
"Sort results by one of the following fields:\n"
|
||||||
"`name`, `-name`, `price`, `-price`, `discount`, `-discount`."
|
"`name`, `-name`, `price`, `-price`, `discount`, `-discount`, `created_at`, `-created_at`."
|
||||||
"\nPrefix with `-` for descending order."
|
"\nPrefix with `-` for descending order."
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -79,32 +113,56 @@ class AllProductsView(APIView):
|
|||||||
required=False,
|
required=False,
|
||||||
type=OpenApiTypes.INT,
|
type=OpenApiTypes.INT,
|
||||||
),
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="in_stock",
|
||||||
|
description="Filter products that are in stock (positive stock).",
|
||||||
|
required=False,
|
||||||
|
type=OpenApiTypes.BOOL,
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="has_discount",
|
||||||
|
description="Filter products that have a discount.",
|
||||||
|
required=False,
|
||||||
|
type=OpenApiTypes.BOOL,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
description=(
|
description=(
|
||||||
"Retrieve products with optional filters and sorting. "
|
"Retrieve products with optional filters and sorting. "
|
||||||
"Provide a category ID to filter products in that category and its subcategories."
|
"Provide a list of category IDs to filter products by those categories and their subcategories."
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: ProductSerializer(many=True),
|
200: ProductSerializer(many=True),
|
||||||
404: OpenApiTypes.OBJECT,
|
404: OpenApiTypes.OBJECT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def get(self, request, pk=None):
|
def get(self, request):
|
||||||
try:
|
try:
|
||||||
if pk:
|
# Get list of category IDs from query parameters
|
||||||
category = Category.objects.get(pk=pk)
|
category_ids = request.query_params.getlist('category', [])
|
||||||
products = ProductModel.objects.filter(category__in=category.get_descendants(include_self=True))
|
if category_ids:
|
||||||
|
# Convert category IDs to integers and filter products by these categories
|
||||||
|
category_ids = [int(id) for id in category_ids]
|
||||||
|
|
||||||
|
products = ProductModel.objects.filter(category__id__in=category_ids)
|
||||||
else:
|
else:
|
||||||
products = ProductModel.objects.all()
|
products = ProductModel.objects.all()
|
||||||
|
|
||||||
|
# Filter by stock status if `in_stock` is specified
|
||||||
|
in_stock = request.query_params.get('in_stock', "false") == 'true'
|
||||||
|
if in_stock:
|
||||||
|
products = products.filter(in_stock__gt=0)
|
||||||
|
|
||||||
|
# Filter by discount if `has_discount` is specified
|
||||||
|
has_discount = request.query_params.get('has_discount', "false") == 'true'
|
||||||
|
if has_discount:
|
||||||
|
products = products.filter(discount__gt=0)
|
||||||
|
|
||||||
|
# Search filter
|
||||||
search_query = request.query_params.get('search', None)
|
search_query = request.query_params.get('search', None)
|
||||||
if search_query:
|
if search_query:
|
||||||
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
|
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
|
||||||
|
|
||||||
category_filter = request.query_params.get('category', None)
|
# Price filters
|
||||||
if category_filter:
|
|
||||||
products = products.filter(category__id=category_filter)
|
|
||||||
|
|
||||||
price_gte = request.query_params.get('price_gte', None)
|
price_gte = request.query_params.get('price_gte', None)
|
||||||
price_lte = request.query_params.get('price_lte', None)
|
price_lte = request.query_params.get('price_lte', None)
|
||||||
if price_gte:
|
if price_gte:
|
||||||
@@ -112,18 +170,20 @@ class AllProductsView(APIView):
|
|||||||
if price_lte:
|
if price_lte:
|
||||||
products = products.filter(price__lte=price_lte)
|
products = products.filter(price__lte=price_lte)
|
||||||
|
|
||||||
|
# Sorting
|
||||||
sort_by = request.query_params.get('sort', None)
|
sort_by = request.query_params.get('sort', None)
|
||||||
if sort_by in ['name', '-name', 'price', '-price', 'discount', '-discount']:
|
if sort_by in ['name', '-name', 'price', '-price', 'discount', '-discount', 'created_at', '-created_at']:
|
||||||
products = products.order_by(sort_by)
|
products = products.order_by(sort_by)
|
||||||
else:
|
else:
|
||||||
products = products.order_by('name')
|
products = products.order_by('name')
|
||||||
|
|
||||||
|
# Pagination
|
||||||
paginator = self.pagination_class()
|
paginator = self.pagination_class()
|
||||||
paginated_products = paginator.paginate_queryset(products, request)
|
paginated_products = paginator.paginate_queryset(products, request)
|
||||||
serializer = self.serializer_class(paginated_products, many=True)
|
serializer = self.serializer_class(paginated_products, many=True)
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
except Category.DoesNotExist:
|
except CategoryModel.DoesNotExist:
|
||||||
return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ dist
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
.logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
|
|||||||
@@ -19,24 +19,24 @@ const { logout } = useAuth();
|
|||||||
const nav_links = ref<NavLink[]>([
|
const nav_links = ref<NavLink[]>([
|
||||||
{
|
{
|
||||||
title: "فروشگاه",
|
title: "فروشگاه",
|
||||||
path: "#",
|
path: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "دسته بندی ها",
|
title: "دسته بندی ها",
|
||||||
path: "#",
|
path: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "جستجو",
|
title: "جستجو",
|
||||||
path: "#",
|
path: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "ارتباط با ما",
|
title: "ارتباط با ما",
|
||||||
path: "#",
|
path: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "امکانات",
|
title: "امکانات",
|
||||||
path: "#",
|
path: "#"
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -53,7 +53,13 @@ const nav_links = ref<NavLink[]>([
|
|||||||
<button @click="() => logout(true)">خروج از وبسایت</button>
|
<button @click="() => logout(true)">خروج از وبسایت</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-black">KIR</div>
|
<button
|
||||||
|
@click="navigateTo('/signin')"
|
||||||
|
class="cursor-pointer"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
وارد شوید
|
||||||
|
</button>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
|
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
// import
|
// import
|
||||||
|
|
||||||
import type { ToastOptions } from "~/composables/useToast";
|
import type { ToastOptions } from "~/composables/global/useToast";
|
||||||
|
import { useToast } from "~/composables/global/useToast";
|
||||||
|
|
||||||
// type
|
// type
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
|||||||
:key="slide.id"
|
:key="slide.id"
|
||||||
>
|
>
|
||||||
<CategoryCard
|
<CategoryCard
|
||||||
|
:id="slide.id"
|
||||||
category="یک دسته بندی تست"
|
category="یک دسته بندی تست"
|
||||||
picture="/img/product-1.jpg"
|
picture="/img/product-1.jpg"
|
||||||
:count="20"
|
:count="20"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
|||||||
import { useIsMutating } from "@tanstack/vue-query";
|
import { useIsMutating } from "@tanstack/vue-query";
|
||||||
import { MUTATION_KEYS } from "~/constants";
|
import { MUTATION_KEYS } from "~/constants";
|
||||||
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
|
||||||
// provide-inject
|
// provide-inject
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ const { isOpen } = inject("isOpen") as any;
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const id = route.params.id as string | number;
|
const id = route.params.id as string | number;
|
||||||
|
|
||||||
@@ -126,55 +129,62 @@ whenever(
|
|||||||
>
|
>
|
||||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||||
|
|
||||||
<Transition name="zoom" mode="out-in">
|
<template v-if="isLoggedIn">
|
||||||
<div
|
<Transition name="zoom" mode="out-in">
|
||||||
v-if="!isChatPending"
|
|
||||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
:style="{
|
v-if="!isChatPending"
|
||||||
|
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
maskImage:
|
maskImage:
|
||||||
'linear-gradient(to top, transparent, black 5%, black, black)',
|
'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||||
}"
|
}"
|
||||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||||
ref="chatContainerEl"
|
ref="chatContainerEl"
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="hasMoreChat"
|
|
||||||
class="py-2 flex items-center justify-center"
|
|
||||||
>
|
>
|
||||||
<Icon name="svg-spinners:3-dots-fade" size="24" />
|
<div
|
||||||
|
v-if="hasMoreChat"
|
||||||
|
class="py-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Icon name="svg-spinners:3-dots-fade" size="24" />
|
||||||
|
</div>
|
||||||
|
<ChatMessage
|
||||||
|
v-for="(message, index) in chatMessages"
|
||||||
|
:key="message.id"
|
||||||
|
:id="message.id"
|
||||||
|
:reverse="message.sender === 'ai'"
|
||||||
|
:content="message.content"
|
||||||
|
:isLast="chatMessages?.length === index + 1"
|
||||||
|
@textUpdate="scrollToBottom"
|
||||||
|
/>
|
||||||
|
<ChatMessage
|
||||||
|
v-if="!!isCreateMessagePending"
|
||||||
|
:id="Date.now() + 1"
|
||||||
|
reverse
|
||||||
|
content=""
|
||||||
|
isLast
|
||||||
|
@textUpdate="scrollToBottom"
|
||||||
|
:loadingContent="!!isCreateMessagePending"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChatMessage
|
|
||||||
v-for="(message, index) in chatMessages"
|
<ChatInput />
|
||||||
:key="message.id"
|
|
||||||
:id="message.id"
|
|
||||||
:reverse="message.sender === 'ai'"
|
|
||||||
:content="message.content"
|
|
||||||
:isLast="chatMessages?.length === index + 1"
|
|
||||||
@textUpdate="scrollToBottom"
|
|
||||||
/>
|
|
||||||
<ChatMessage
|
|
||||||
v-if="!!isCreateMessagePending"
|
|
||||||
:id="Date.now() + 1"
|
|
||||||
reverse
|
|
||||||
content=""
|
|
||||||
isLast
|
|
||||||
@textUpdate="scrollToBottom"
|
|
||||||
:loadingContent="!!isCreateMessagePending"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChatInput />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||||
>
|
>
|
||||||
<AiLoading />
|
<AiLoading />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<div class="text-black p-4.5 size-full flex justify-center items-center" v-else>
|
||||||
|
Please sign in first
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const useAuth = () => {
|
|||||||
// state
|
// state
|
||||||
|
|
||||||
const token = useCookie("token");
|
const token = useCookie("token");
|
||||||
|
const refreshToken = useCookie("refresh-token");
|
||||||
|
|
||||||
// method
|
// method
|
||||||
|
|
||||||
@@ -10,11 +11,20 @@ export const useAuth = () => {
|
|||||||
token.value = newToken;
|
token.value = newToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateRefreshToken = (newToken: string) => {
|
||||||
|
refreshToken.value = newToken;
|
||||||
|
};
|
||||||
|
|
||||||
const logout = (reload ?: boolean) => {
|
const logout = (reload ?: boolean) => {
|
||||||
token.value = undefined;
|
token.value = undefined;
|
||||||
|
refreshToken.value = undefined;
|
||||||
if (reload) window.location.reload();
|
if (reload) window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
return { token, updateToken, logout };
|
// computed
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value);
|
||||||
|
|
||||||
|
return { token, refreshToken, updateRefreshToken, updateToken, logout, isLoggedIn };
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type RefreshAuthRequest = {
|
||||||
|
refresh: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefreshAuthResponse = {
|
||||||
|
access: string,
|
||||||
|
refresh: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const useRefreshAuth = () => {
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleRefreshAuth = async (variables: RefreshAuthRequest) => {
|
||||||
|
const { data } = await axios.post<RefreshAuthResponse>(`${API_ENDPOINTS.auth.refresh}/`, variables);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: RefreshAuthRequest) => handleRefreshAuth(variables)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRefreshAuth;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type VerifyRequest = {
|
||||||
|
token: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const useVerify = () => {
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleVerify = async (variables: VerifyRequest) => {
|
||||||
|
const { data } = await axios.post(`${API_ENDPOINTS.auth.verify}`, variables);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: VerifyRequest) => handleVerify(variables)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useVerify;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useInfiniteQuery } from "@tanstack/vue-query";
|
import { useInfiniteQuery } from "@tanstack/vue-query";
|
||||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ const useGetBranch = (
|
|||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
// method
|
// method
|
||||||
|
|
||||||
const handleGetChat = async ({ productId, limit, offset }: {
|
const handleGetChat = async ({ productId, limit, offset }: {
|
||||||
@@ -37,7 +40,7 @@ const useGetBranch = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
enabled,
|
enabled: isLoggedIn,
|
||||||
queryKey: [QUERY_KEYS.chat],
|
queryKey: [QUERY_KEYS.chat],
|
||||||
initialPageParam: {
|
initialPageParam: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const API_ENDPOINTS = {
|
|||||||
get: "/products",
|
get: "/products",
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
|
refresh: "/token/refresh",
|
||||||
|
verify: "/accounts/verify",
|
||||||
signin: "/token",
|
signin: "/token",
|
||||||
logout: "/accounts/logout",
|
logout: "/accounts/logout",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,70 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
// import
|
||||||
|
|
||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
import useRefreshAuth from "~/composables/api/auth/useRefreshAuth";
|
||||||
|
import useVerify from "~/composables/api/auth/useVerify";
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { mutateAsync: refreshAuth } = useRefreshAuth();
|
||||||
|
const { token, refreshToken, updateToken, updateRefreshToken, logout } = useAuth();
|
||||||
|
const { mutateAsync: verify } = useVerify();
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onServerPrefetch(async () => {
|
||||||
|
if (!!token.value) {
|
||||||
|
|
||||||
|
// 1.1 - token is there
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
await verify({
|
||||||
|
token: token.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.1 - token is valid, finish
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
// 2.2 - token is there, but not valid, try to refresh token
|
||||||
|
|
||||||
|
if (!!refreshToken.value) {
|
||||||
|
|
||||||
|
// 3.1 - refresh token is there, try to refresh
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshResponse = await refreshAuth({ refresh: refreshToken.value });
|
||||||
|
|
||||||
|
// 4.1 - token is refreshed successfully, finish
|
||||||
|
|
||||||
|
updateToken(refreshResponse.access);
|
||||||
|
updateRefreshToken(refreshResponse.refresh);
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
// 4.2 - cant refreshing token, logout
|
||||||
|
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 3.2 - refresh token is not exist, logout
|
||||||
|
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 1.2 - token is not exist, logout
|
||||||
|
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
export default defineNuxtRouteMiddleware(() => {
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
const { token } = useAuth();
|
|
||||||
|
|
||||||
if (token.value) {
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const { token, logout } = useAuth();
|
||||||
|
|
||||||
|
if (!!token.value) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
logout();
|
||||||
return navigateTo("/signin");
|
return navigateTo("/signin");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
|
||||||
export default defineNuxtRouteMiddleware(() => {
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|||||||
@@ -28,11 +28,10 @@ definePageMeta({
|
|||||||
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const { updateToken } = useAuth();
|
const { updateToken, updateRefreshToken } = useAuth();
|
||||||
|
|
||||||
const { refetch: refetchAccount } = useGetAccount();
|
const { refetch: refetchAccount } = useGetAccount();
|
||||||
|
|
||||||
|
|
||||||
const showOtp = ref(false);
|
const showOtp = ref(false);
|
||||||
const otpCode = ref([]);
|
const otpCode = ref([]);
|
||||||
|
|
||||||
@@ -107,6 +106,8 @@ const handleLogin = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateToken(response.access);
|
updateToken(response.access);
|
||||||
|
updateRefreshToken(response.refresh);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
await refetchAccount();
|
await refetchAccount();
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
@@ -116,9 +117,7 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
navigateTo("/");
|
||||||
navigateTo("/");
|
|
||||||
}, 2000);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
otpCode.value = [];
|
otpCode.value = [];
|
||||||
addToast({ message: "مشکلی پیش آمده" });
|
addToast({ message: "مشکلی پیش آمده" });
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { API_ENDPOINTS } from "~/constants";
|
|||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const { token } = useAuth();
|
const { token, logout } = useAuth();
|
||||||
|
|
||||||
const axios = axiosOriginal.create({
|
const axios = axiosOriginal.create({
|
||||||
baseURL: config.public.API_BASE_URL,
|
baseURL: config.public.API_BASE_URL
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
@@ -25,9 +25,9 @@ export default defineNuxtPlugin(() => {
|
|||||||
return response;
|
return response;
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
|
|
||||||
if (error.status === 401) {
|
// if (error.status === 401) {
|
||||||
logout();
|
// logout();
|
||||||
}
|
// }
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
type LogType = {
|
||||||
|
title: string;
|
||||||
|
status?: "success" | "error" | "info" | "warning";
|
||||||
|
message?: string,
|
||||||
|
details?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private static formatToMarkdown(log: LogType) {
|
||||||
|
const date = new Date();
|
||||||
|
let month = "" + (date.getMonth() + 1);
|
||||||
|
let day = "" + date.getDate();
|
||||||
|
let year = date.getFullYear();
|
||||||
|
let hour = date.getHours();
|
||||||
|
let minutes = date.getMinutes();
|
||||||
|
let seconds = date.getSeconds();
|
||||||
|
|
||||||
|
if (month.length < 2) {
|
||||||
|
month = "0" + month;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day.length < 2) {
|
||||||
|
day = "0" + day;
|
||||||
|
}
|
||||||
|
|
||||||
|
let markdownContent = "";
|
||||||
|
|
||||||
|
let icon = "ℹ️";
|
||||||
|
|
||||||
|
switch (log.status) {
|
||||||
|
case "info":
|
||||||
|
icon = "ℹ️";
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
icon = "‼️";
|
||||||
|
break;
|
||||||
|
case "warning":
|
||||||
|
icon = "⚠️";
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
icon = "✅";
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
icon = "ℹ️";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownContent += `# ${icon} ${log.title} \n`;
|
||||||
|
markdownContent += `## ${[year, month, day].join("-")} ${hour}:${minutes}:${seconds} \n`;
|
||||||
|
|
||||||
|
if (log.message) {
|
||||||
|
markdownContent += `**Message:** ${log.message}\n`;
|
||||||
|
}
|
||||||
|
if (log.details) {
|
||||||
|
markdownContent += `**Details:**\n\n\`\`\`json\n${JSON.stringify(log.details, null, 2)}\n\`\`\`\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownContent += "<br></br>\n\n";
|
||||||
|
markdownContent += "---\n";
|
||||||
|
|
||||||
|
return markdownContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async log(info: LogType) {
|
||||||
|
const formattedLog = this.formatToMarkdown(info);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.appendFile(".logs/log.md", formattedLog);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logger;
|
||||||
Reference in New Issue
Block a user