diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6c287c1..aebe0d1 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -8,10 +8,11 @@ on: jobs: deploy: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Copy files to server uses: appleboy/scp-action@v0.1.6 @@ -21,8 +22,19 @@ jobs: password: ${{ secrets.SSH_PASSWORD }} source: "." target: "/root/hshop/" + rm: true - - name: SSH command to build and start Docker + - name: Deploy environment file + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + script: | + mkdir -p /root/hshop/backend/ + printf "%s" "${{ secrets.ENV_FILE_CONTENT }}" > /root/hshop/backend/.env.local + + - name: Build and start Docker containers uses: appleboy/ssh-action@v0.1.6 with: host: ${{ secrets.SERVER_HOST }} @@ -30,6 +42,11 @@ jobs: password: ${{ secrets.SSH_PASSWORD }} script: | cd /root/hshop/ - docker compose down - docker compose build - docker compose up -d \ No newline at end of file + + docker compose down --remove-orphans --timeout 60 + + docker compose up --build --detach --remove-orphans + + docker image prune -af + + docker compose ps \ No newline at end of file diff --git a/backend/account/models.py b/backend/account/models.py index b270618..da7d7cf 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -241,4 +241,11 @@ class SecurityBreachAttemptModel(models.Model): return f'تلاش نفوذ از {self.ip_address} در {self.city}, {self.country}' class Meta: verbose_name = "تلاش نفوذ" - verbose_name_plural = "تلاش‌های نفوذ" \ No newline at end of file + verbose_name_plural = "تلاش‌های نفوذ" + +# class NotifModel(models.Model): +# subject = models.CharField(max_length=100) +# description = models.TextField() + +# def __str__(self): +# return f'{self.subject[:30]}' \ No newline at end of file diff --git a/backend/account/urls.py b/backend/account/urls.py index f3cb639..958cb0c 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -2,6 +2,8 @@ from django.urls import path from . import views 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'), @@ -13,6 +15,7 @@ urlpatterns = [ path('address/list', views.GetUserAddressesView.as_view(), name='list-addresses'), path('address/', views.GetIDUserAddressView.as_view(), name='get-ID-address'), path('subscribe', views.SubscribeView.as_view(), name='subscibe'), + path('unsubscribe', views.UnsubscribeView.as_view(), name='unsubscibe'), path('attack/view/', views.ChangeViewAttack.as_view(), name='attack-view'), path('logout', views.LogoutView.as_view(), name='logout'), ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index 2b52687..9799d2d 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -11,12 +11,11 @@ from django.shortcuts import get_object_or_404, redirect from rest_framework_simplejwt.tokens import RefreshToken import ghasedak_sms from django.views import View -# this works only need to be used -# class APIView(APIView): -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# if AllowAny in self.permission_classes or not self.permission_classes: -# self.authentication_classes = [] +from rest_framework import serializers +from rest_framework_simplejwt.tokens import RefreshToken + + + class SendOTPView(APIView): permission_classes = [AllowAny] @extend_schema( @@ -143,7 +142,9 @@ class CreateAddressView(generics.CreateAPIView): permission_classes = [permissions.IsAuthenticated] def perform_create(self, serializer): - serializer.save(user=self.request.user) + user = self.request.user + is_first_address = not UserAddressModel.objects.filter(user=user).exists() + serializer.save(user=user, is_main=is_first_address) class EditAddressView(generics.UpdateAPIView): queryset = UserAddressModel.objects.all() @@ -190,6 +191,25 @@ class SubscribeView(APIView): return Response(status=status.HTTP_400_BAD_REQUEST) +class UnsubscribeSerializer(serializers.Serializer): + end_point = serializers.CharField() + +class UnsubscribeView(APIView): + permission_classes = [IsAuthenticated] + serializer_class = UnsubscribeSerializer + def post(self, request): + endpoint = request.data.get("end_point") + if not endpoint: + return Response({"detail": "اند پوینت لازم است"}, status=status.HTTP_400_BAD_REQUEST) + + deleted, _ = PushSubscription.objects.filter(user=request.user, endpoint=endpoint).delete() + + if deleted: + return Response({"detail": "با موفقیت اشتراک پاک شد"}, status=status.HTTP_200_OK) + + return Response({"detail": "اند پوینت پیدا نشد"}, status=status.HTTP_404_NOT_FOUND) + + class ChangeViewAttack(View): def get(self, request, pk): attack = get_object_or_404(SecurityBreachAttemptModel, pk=pk) @@ -198,8 +218,6 @@ class ChangeViewAttack(View): return redirect('admin:account_securitybreachattemptmodel_changelist') -from rest_framework import serializers -from rest_framework_simplejwt.tokens import RefreshToken class LogoutSerializer(serializers.Serializer): refresh_token = serializers.CharField(help_text="Refresh token to be blacklisted") diff --git a/backend/azbankgateways/migrations/0007_alter_bank_order.py b/backend/azbankgateways/migrations/0007_alter_bank_order.py new file mode 100644 index 0000000..dc9f777 --- /dev/null +++ b/backend/azbankgateways/migrations/0007_alter_bank_order.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-03-19 17:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('azbankgateways', '0006_bank_order'), + ('order', '0023_remove_ordermodel_bank_records'), + ] + + operations = [ + migrations.AlterField( + model_name='bank', + name='order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bank_records', to='order.ordermodel'), + ), + ] diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 1c00999..ababbb6 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -143,7 +143,7 @@ USE_TZ = True # Static Files Configuration # ============================================================================== STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "custom_static"), + # os.path.join(BASE_DIR, "custom_static"), BASE_DIR / "core" / "static", ] diff --git a/backend/order/apps.py b/backend/order/apps.py index 60f4dff..eca896d 100644 --- a/backend/order/apps.py +++ b/backend/order/apps.py @@ -5,3 +5,6 @@ class OrderConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'order' verbose_name = 'سفارش' + + def ready(self): + import order.signals \ No newline at end of file diff --git a/backend/order/execptions.py b/backend/order/execptions.py deleted file mode 100644 index c92b966..0000000 --- a/backend/order/execptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class DiscountNotAvailableError(Exception): - pass \ No newline at end of file diff --git a/backend/order/models.py b/backend/order/models.py index 5c27f6c..25049ed 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -2,7 +2,6 @@ from django.db import models from account.models import User, UserAddressModel, PushSubscription from product.models import ProductModel, ProductVariant, ProductImageModel from django.utils import timezone -from .execptions import DiscountNotAvailableError from django_jalali.db import models as jmodels diff --git a/backend/order/permissons.py b/backend/order/permissons.py index af60575..bef19d4 100644 --- a/backend/order/permissons.py +++ b/backend/order/permissons.py @@ -20,7 +20,21 @@ class GetOrderPermission(BasePermission): def has_object_permission(self, request, view, obj): if obj.user != request.user: return False - if obj.status != 'CART': + if obj.status == 'CART': self.message = "سفارش در وضعیت سبد خرید است" return False - return True \ No newline at end of file + return True + + + +from rest_framework.permissions import BasePermission + +class SetAddressPermissions(BasePermission): + message = "این ادرس متعلق به شما نیست." + + def has_object_permission(self, request, view, obj): + if obj.user != request.user: + self.message = "این ادرس متعلق به شما نیست." + return False + return True + \ No newline at end of file diff --git a/backend/order/signals.py b/backend/order/signals.py new file mode 100644 index 0000000..7d53ab5 --- /dev/null +++ b/backend/order/signals.py @@ -0,0 +1,24 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver +from .models import OrderModel + +@receiver(pre_save, sender=OrderModel) +def order_status_changed(sender, instance, **kwargs): + if instance.pk: + previous = OrderModel.objects.get(pk=instance.pk) + + if previous.status != instance.status: + send_change_status_notif(instance) + + +def send_change_status_notif(order): + pass + +def update_cart_price_fields(order): + pass + +def update_sell_data(order): + pass + +def update_quantity(order): + pass diff --git a/backend/order/urls.py b/backend/order/urls.py index 969c612..6ec9fd4 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -1,16 +1,17 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView +from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView, SetAddressForCartView from .views import PaymentView, callback_view urlpatterns = [ path('all', OrderlistView.as_view(), name='order-list'), path('cart', CartView.as_view()), + path('cart/set-address', SetAddressForCartView.as_view()), path('cart/discount', ApplyDiscountView.as_view()), path('cart/all', CartItemClear.as_view()), path('cart/item/', CartItemViews.as_view(), name='change-item-cart'), - path('payment', PaymentView.as_view(), name='payment'), + path('cart/payment', PaymentView.as_view(), name='payment'), path('callback', callback_view, name='callback-gateway'), path('', OrderGetView.as_view(), name='order-get'), ] diff --git a/backend/order/views.py b/backend/order/views.py index e2425fe..5e32129 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -1,5 +1,4 @@ from django.shortcuts import render -from .execptions import DiscountNotAvailableError from rest_framework.views import APIView, Response from django.shortcuts import get_object_or_404 from product.models import ProductVariant @@ -8,25 +7,20 @@ from .serializers import * # from cart.models import from rest_framework import status from .models import OrderItemModel, OrderModel, DiscountCode -from .permissons import CanDeleteCartItemPermissions, GetOrderPermission +from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions from azbankgateways import bankfactories, models as bank_models from azbankgateways.exceptions import AZBankGatewaysException from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from utils.pagination import StructurePagination from order.models import OrderModel -try: - pass -except DiscountNotAvailableError: - pass from django.urls import reverse -""" +from account.models import UserAddressModel -add post -remove delete -show get -pay -""" +# try: +# pass +# except DiscountNotAvailableError: +# pass @@ -185,9 +179,22 @@ class OrderGetView(APIView): order_ser = self.serializer_class(order_object, context={'request': request}) return Response(order_ser.data, status=status.HTTP_200_OK) + + +from rest_framework import serializers + +class BankTypeSerializer(serializers.Serializer): + gateway_type = serializers.ChoiceField(choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']) + + class PaymentView(APIView): permission_classes = [IsAuthenticated] + serializer_class = BankTypeSerializer + @extend_schema( + description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']" + ) def post(self, request): + print(request.data.get('gateway_type')) cart_order = get_object_or_404(OrderModel, user=request.user, status='CART') amount = 5000 user_mobile_number = request.user.phone @@ -245,4 +252,29 @@ def callback_view(request): return HttpResponse( "پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت." - ) \ No newline at end of file + ) + + + +class SetAddressSerilizer(serializers.Serializer): + address_id = serializers.IntegerField() + +class SetAddressForCartView(APIView): + serializer_class = SetAddressSerilizer + permission_classes = [IsAuthenticated, SetAddressPermissions] + def post(self, request): + address_id = request.data.get('address_id', None) + if not address_id: + return Response({'detail': 'address_id را ارسال کنید'}, status=status.HTTP_400_BAD_REQUEST) + address_object = get_object_or_404(UserAddressModel, pk=address_id) + permission = SetAddressPermissions() + if not permission.has_object_permission(request, self, address_object): + return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) + + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + cart_order.address = address_object + cart_order.save() + return Response({'detail': 'ادرس با موفقیت انتخاب شد'}) \ No newline at end of file diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 135e7da..e5890f8 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -50,6 +50,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer): images = ProductImageSerailizer(many=True) details = ProductDetailSerializer(many=True, read_only=True) cart_quantity = serializers.SerializerMethodField() + price = serializers.SerializerMethodField() class Meta: model = ProductVariant exclude = ('min_price', 'sell', 'currency', 'product', 'input_price') @@ -72,6 +73,8 @@ class ProductVariantSerialzier(serializers.ModelSerializer): return item['quantity'] return 0 + def get_pirce(self, obj): + return f'{obj.price:,.0f} تومان' class SubCategorySerializer(serializers.ModelSerializer): diff --git a/frontend/components/global/Avatar.vue b/frontend/components/global/Avatar.vue index e1a3a25..025a739 100644 --- a/frontend/components/global/Avatar.vue +++ b/frontend/components/global/Avatar.vue @@ -23,7 +23,7 @@ const { isLoading } = useImage({ src: src.value });