merge
This commit is contained in:
@@ -4,6 +4,7 @@ from djoser.urls.jwt import views as djoser_jwt_views
|
||||
|
||||
urlpatterns = [
|
||||
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('address/create', views.CreateAddressView.as_view(), name='create-address'),
|
||||
path('address/edit/<int:pk>', views.EditAddressView.as_view(), name='edit-address'),
|
||||
|
||||
@@ -200,8 +200,8 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=2),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ urlpatterns = [
|
||||
|
||||
|
||||
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('schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
# 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_keywords = models.CharField(max_length=300, blank=True, null=True, 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):
|
||||
discount_price = int(self.price * (100 - self.discount) / 100)
|
||||
formatted_num = "{:,.0f}".format(discount_price)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from .models import *
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
class ProductChatSerializer(serializers.ModelSerializer):
|
||||
price = serializers.SerializerMethodField()
|
||||
is_new = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = ProductModel
|
||||
fields = ['name', 'description', 'price', 'in_stock', 'discount', ]
|
||||
@@ -13,11 +16,14 @@ class ProductChatSerializer(serializers.ModelSerializer):
|
||||
if dollor_price is None:
|
||||
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
|
||||
if obj.currency == 'toman':
|
||||
return obj.price
|
||||
toman_price = obj.price
|
||||
elif obj.currency == 'dollor':
|
||||
return obj.price * dollor_price
|
||||
toman_price = obj.price * dollor_price
|
||||
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 Meta:
|
||||
|
||||
+78
-18
@@ -8,28 +8,60 @@ from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
from utils.pagination import StructurePagination
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
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):
|
||||
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):
|
||||
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)
|
||||
return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK)
|
||||
|
||||
class ProductView(APIView):
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
def get(self, request, pk):
|
||||
product = get_object_or_404(ProductModel, id=pk)
|
||||
product_ser = self.serializer_class(instance=product, many=False)
|
||||
return Response(product_ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AllProductsView(APIView):
|
||||
serializer_class = ProductSerializer
|
||||
pagination_class = StructurePagination
|
||||
authentication_classes = [] # Add authentication if required
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
@@ -41,10 +73,12 @@ class AllProductsView(APIView):
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="category",
|
||||
description="Filter by category ID.",
|
||||
type={'type': 'array', 'items': {'type': 'number'}},
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
type=OpenApiTypes.INT,
|
||||
),
|
||||
style='form',
|
||||
explode=False,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="price_gte",
|
||||
description="Filter products with price greater than or equal to this value.",
|
||||
@@ -61,7 +95,7 @@ class AllProductsView(APIView):
|
||||
name="sort",
|
||||
description=(
|
||||
"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."
|
||||
),
|
||||
required=False,
|
||||
@@ -79,32 +113,56 @@ class AllProductsView(APIView):
|
||||
required=False,
|
||||
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=(
|
||||
"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={
|
||||
200: ProductSerializer(many=True),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
)
|
||||
def get(self, request, pk=None):
|
||||
def get(self, request):
|
||||
try:
|
||||
if pk:
|
||||
category = Category.objects.get(pk=pk)
|
||||
products = ProductModel.objects.filter(category__in=category.get_descendants(include_self=True))
|
||||
# Get list of category IDs from query parameters
|
||||
category_ids = request.query_params.getlist('category', [])
|
||||
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:
|
||||
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)
|
||||
if search_query:
|
||||
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
|
||||
|
||||
category_filter = request.query_params.get('category', None)
|
||||
if category_filter:
|
||||
products = products.filter(category__id=category_filter)
|
||||
|
||||
# Price filters
|
||||
price_gte = request.query_params.get('price_gte', None)
|
||||
price_lte = request.query_params.get('price_lte', None)
|
||||
if price_gte:
|
||||
@@ -112,18 +170,20 @@ class AllProductsView(APIView):
|
||||
if price_lte:
|
||||
products = products.filter(price__lte=price_lte)
|
||||
|
||||
# Sorting
|
||||
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)
|
||||
else:
|
||||
products = products.order_by('name')
|
||||
|
||||
# Pagination
|
||||
paginator = self.pagination_class()
|
||||
paginated_products = paginator.paginate_queryset(products, request)
|
||||
serializer = self.serializer_class(paginated_products, many=True)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ dist
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
.logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
|
||||
@@ -19,24 +19,24 @@ const { logout } = useAuth();
|
||||
const nav_links = ref<NavLink[]>([
|
||||
{
|
||||
title: "فروشگاه",
|
||||
path: "#",
|
||||
path: "#"
|
||||
},
|
||||
{
|
||||
title: "دسته بندی ها",
|
||||
path: "#",
|
||||
path: "#"
|
||||
},
|
||||
{
|
||||
title: "جستجو",
|
||||
path: "#",
|
||||
path: "#"
|
||||
},
|
||||
{
|
||||
title: "ارتباط با ما",
|
||||
path: "#",
|
||||
path: "#"
|
||||
},
|
||||
{
|
||||
title: "امکانات",
|
||||
path: "#",
|
||||
},
|
||||
path: "#"
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -53,7 +53,13 @@ const nav_links = ref<NavLink[]>([
|
||||
<button @click="() => logout(true)">خروج از وبسایت</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-black">KIR</div>
|
||||
<button
|
||||
@click="navigateTo('/signin')"
|
||||
class="cursor-pointer"
|
||||
v-else
|
||||
>
|
||||
وارد شوید
|
||||
</button>
|
||||
|
||||
<nav
|
||||
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
// import
|
||||
|
||||
import type { ToastOptions } from "~/composables/useToast";
|
||||
import type { ToastOptions } from "~/composables/global/useToast";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
// type
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
:key="slide.id"
|
||||
>
|
||||
<CategoryCard
|
||||
:id="slide.id"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
|
||||
@@ -7,6 +7,7 @@ import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
||||
import { useIsMutating } from "@tanstack/vue-query";
|
||||
import { MUTATION_KEYS } from "~/constants";
|
||||
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// provide-inject
|
||||
|
||||
@@ -14,6 +15,8 @@ const { isOpen } = inject("isOpen") as any;
|
||||
|
||||
// state
|
||||
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string | number;
|
||||
|
||||
@@ -126,55 +129,62 @@ whenever(
|
||||
>
|
||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<div
|
||||
v-if="!isChatPending"
|
||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||
>
|
||||
<template v-if="isLoggedIn">
|
||||
<Transition name="zoom" mode="out-in">
|
||||
|
||||
<div
|
||||
:style="{
|
||||
v-if="!isChatPending"
|
||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
maskImage:
|
||||
'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||
}"
|
||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||
ref="chatContainerEl"
|
||||
>
|
||||
<div
|
||||
v-if="hasMoreChat"
|
||||
class="py-2 flex items-center justify-center"
|
||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||
ref="chatContainerEl"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
/>
|
||||
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||
>
|
||||
<AiLoading />
|
||||
</div>
|
||||
</Transition>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||
>
|
||||
<AiLoading />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<div class="text-black p-4.5 size-full flex justify-center items-center" v-else>
|
||||
Please sign in first
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// types
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ export const useAuth = () => {
|
||||
// state
|
||||
|
||||
const token = useCookie("token");
|
||||
const refreshToken = useCookie("refresh-token");
|
||||
|
||||
// method
|
||||
|
||||
@@ -10,11 +11,20 @@ export const useAuth = () => {
|
||||
token.value = newToken;
|
||||
};
|
||||
|
||||
const updateRefreshToken = (newToken: string) => {
|
||||
refreshToken.value = newToken;
|
||||
};
|
||||
|
||||
const logout = (reload ?: boolean) => {
|
||||
token.value = undefined;
|
||||
refreshToken.value = undefined;
|
||||
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 { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// types
|
||||
|
||||
@@ -16,6 +17,8 @@ const useGetBranch = (
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
// method
|
||||
|
||||
const handleGetChat = async ({ productId, limit, offset }: {
|
||||
@@ -37,7 +40,7 @@ const useGetBranch = (
|
||||
};
|
||||
|
||||
return useInfiniteQuery({
|
||||
enabled,
|
||||
enabled: isLoggedIn,
|
||||
queryKey: [QUERY_KEYS.chat],
|
||||
initialPageParam: {
|
||||
limit: 10,
|
||||
|
||||
@@ -7,6 +7,8 @@ export const API_ENDPOINTS = {
|
||||
get: "/products",
|
||||
},
|
||||
auth: {
|
||||
refresh: "/token/refresh",
|
||||
verify: "/accounts/verify",
|
||||
signin: "/token",
|
||||
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>
|
||||
<div
|
||||
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { token } = useAuth();
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
if (token.value) {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { token, logout } = useAuth();
|
||||
|
||||
if (!!token.value) {
|
||||
return;
|
||||
} else {
|
||||
logout();
|
||||
return navigateTo("/signin");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
|
||||
const { token } = useAuth();
|
||||
|
||||
@@ -28,11 +28,10 @@ definePageMeta({
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const { updateToken } = useAuth();
|
||||
const { updateToken, updateRefreshToken } = useAuth();
|
||||
|
||||
const { refetch: refetchAccount } = useGetAccount();
|
||||
|
||||
|
||||
const showOtp = ref(false);
|
||||
const otpCode = ref([]);
|
||||
|
||||
@@ -107,6 +106,8 @@ const handleLogin = async () => {
|
||||
});
|
||||
|
||||
updateToken(response.access);
|
||||
updateRefreshToken(response.refresh);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await refetchAccount();
|
||||
|
||||
addToast({
|
||||
@@ -116,9 +117,7 @@ const handleLogin = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo("/");
|
||||
}, 2000);
|
||||
navigateTo("/");
|
||||
} catch (e) {
|
||||
otpCode.value = [];
|
||||
addToast({ message: "مشکلی پیش آمده" });
|
||||
|
||||
@@ -4,10 +4,10 @@ import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig();
|
||||
const { token } = useAuth();
|
||||
const { token, logout } = useAuth();
|
||||
|
||||
const axios = axiosOriginal.create({
|
||||
baseURL: config.public.API_BASE_URL,
|
||||
baseURL: config.public.API_BASE_URL
|
||||
});
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
@@ -25,9 +25,9 @@ export default defineNuxtPlugin(() => {
|
||||
return response;
|
||||
}, function(error) {
|
||||
|
||||
if (error.status === 401) {
|
||||
logout();
|
||||
}
|
||||
// if (error.status === 401) {
|
||||
// logout();
|
||||
// }
|
||||
|
||||
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