Already have an account? Login
diff --git a/Frontend/src/components/Pages/Signup.jsx b/Frontend/src/components/Pages/Signup.jsx
deleted file mode 100644
index b4082d9..0000000
--- a/Frontend/src/components/Pages/Signup.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-
-function Signup() {
- return (
-
Signup
- )
-}
-
-export default Signup
\ No newline at end of file
diff --git a/Frontend/src/components/Payment/Failure.jsx b/Frontend/src/components/Payment/Failure.jsx
new file mode 100644
index 0000000..b7d5742
--- /dev/null
+++ b/Frontend/src/components/Payment/Failure.jsx
@@ -0,0 +1,9 @@
+import React from 'react'
+
+function Failure() {
+ return (
+
Transaction Failure
+ )
+}
+
+export default Failure
\ No newline at end of file
diff --git a/Frontend/src/components/Payment/PaymentGateway.jsx b/Frontend/src/components/Payment/PaymentGateway.jsx
new file mode 100644
index 0000000..93fd01e
--- /dev/null
+++ b/Frontend/src/components/Payment/PaymentGateway.jsx
@@ -0,0 +1,217 @@
+
+
+
+
+import React, { useState, useEffect } from "react";
+import { v4 as uuidv4 } from "uuid";
+import CryptoJS from "crypto-js";
+import { useLocation } from "react-router-dom";
+
+const PaymentGateway = () => {
+ const location = useLocation();
+ const initialTotal = location.state?.total || "10";
+
+ const [formData, setFormData] = useState({
+ amount: initialTotal,
+ tax_amount: "0",
+ total_amount: initialTotal,
+ transaction_uuid: uuidv4(),
+ product_service_charge: "0",
+ product_delivery_charge: "0",
+ product_code: "EPAYTEST",
+ success_url: "http://localhost:5173/success",
+ failure_url: "http://localhost:5173/failure",
+ signed_field_names: "total_amount,transaction_uuid,product_code",
+ signature: "",
+ });
+
+ const [userDetails, setUserDetails] = useState({
+ firstName: "",
+ lastName: "",
+ email: "",
+ mobile: "",
+ address: "",
+ paymentMethod: "esewa",
+ });
+
+ const generateSignature = (data) => {
+ const secret = "8gBm/:&EnhH.1/q";
+ const hashString = `total_amount=${data.total_amount},transaction_uuid=${data.transaction_uuid},product_code=${data.product_code}`;
+ const hash = CryptoJS.HmacSHA256(hashString, secret);
+ return CryptoJS.enc.Base64.stringify(hash);
+ };
+
+ useEffect(() => {
+ const signature = generateSignature(formData);
+ setFormData((prev) => ({ ...prev, signature }));
+ }, [formData.total_amount, formData.transaction_uuid, formData.product_code]);
+
+ const handleAmountChange = (e) => {
+ const value = e.target.value;
+ setFormData((prev) => ({
+ ...prev,
+ amount: value,
+ total_amount: value,
+ }));
+ };
+
+ return (
+
+ {/* Checkout Form */}
+
+
+
+
+
Your Products
+
+ {location.state?.cartItems?.map((item, idx) => (
+
+
+
+
{item.product.name}
+
Category: {item.product.category || 'N/A'}
+
Qty: {item.quantity}
+
{location.state.currency} {item.product.price}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default PaymentGateway;
diff --git a/Frontend/src/components/Payment/Success.jsx b/Frontend/src/components/Payment/Success.jsx
new file mode 100644
index 0000000..349f313
--- /dev/null
+++ b/Frontend/src/components/Payment/Success.jsx
@@ -0,0 +1,46 @@
+// export default Success;
+import React, { useEffect, useState } from "react";
+import { useSearchParams } from "react-router-dom"; // Correct import
+import check from "./check.png"
+const Success = () => {
+ const [searchParams] = useSearchParams();
+ const dataQuery = searchParams.get("data");
+ const [data, setData] = useState({});
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ try {
+ if (dataQuery) {
+ const resData = atob(dataQuery);
+ const resObject = JSON.parse(resData);
+ console.log(resObject);
+ setData(resObject);
+ } else {
+ setError("No data parameter found in URL");
+ }
+ } catch (err) {
+ console.error("Error parsing data:", err);
+ setError("Failed to parse payment data");
+ }
+ }, [dataQuery]);
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
Rs. {data.total_amount || "0"}
+
Payment Successful
+
+
+ );
+};
+
+export default Success;
\ No newline at end of file
diff --git a/Frontend/src/components/Payment/check.png b/Frontend/src/components/Payment/check.png
new file mode 100644
index 0000000..6aa84d9
Binary files /dev/null and b/Frontend/src/components/Payment/check.png differ
diff --git a/Frontend/src/components/Payment/successful.png b/Frontend/src/components/Payment/successful.png
new file mode 100644
index 0000000..5cdb890
Binary files /dev/null and b/Frontend/src/components/Payment/successful.png differ
diff --git a/Frontend/src/components/api/axiosInstance.jsx b/Frontend/src/components/api/axiosInstance.jsx
new file mode 100644
index 0000000..e55087e
--- /dev/null
+++ b/Frontend/src/components/api/axiosInstance.jsx
@@ -0,0 +1,12 @@
+// src/api/axiosInstance.js
+import axios from 'axios';
+import React from 'react';
+
+const instance = axios.create({
+ baseURL: 'http://localhost:8000', // update with your backend URL
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+export default instance;
diff --git a/backend/app/esewa_utils.py b/backend/app/esewa_utils.py
new file mode 100644
index 0000000..d1f7783
--- /dev/null
+++ b/backend/app/esewa_utils.py
@@ -0,0 +1,32 @@
+import hashlib
+import uuid
+from django.conf import settings
+
+def generate_esewa_payment_data(amount, order_id, service_charge = 0):
+ data = {
+ 'amount' : amount,
+ 'product_delivery_charge': 0,
+ 'product_service_charge': service_charge,
+ 'tax_amount': 0,
+ 'total_amount': float(amount) + float(service_charge),
+ 'transaction_uuid': order_id,
+ 'product_code': settings.ESEWA_MERCHANT_ID,
+ 'success_url': settings.ESEWA_SUCCESS_URL,
+ 'failure_url': settings.ESEWA_FAILURE_URL,
+
+ }
+ return data
+
+def verify_esewa_payment(pid, refId,amount):
+ import requests
+ data = {
+ 'amount': amount,
+ 'transaction_id': pid,
+ 'product_code': settings.ESEWA_MERCHANT_ID
+
+ }
+ response = requests.post(settings.ESEWA_VERIFY_URL, data = data)
+ if response.status_code == 200:
+ res_data = response.json()
+ return res_data.get('status') == "COMPLETE"
+ return False
\ No newline at end of file
diff --git a/backend/app/models.py b/backend/app/models.py
index 17f711d..3e61af3 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -21,7 +21,7 @@ class Product(models.Model):
description = models.TextField()
brand = models.CharField()
ingredients = models.TextField()
- img = models.ImageField(upload_to='images')
+ img = models.ImageField(upload_to='images/')
class User(AbstractUser):
@@ -41,3 +41,21 @@ class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete = models.CASCADE)
quantity = models.PositiveIntegerField(default = 1)
+
+class Order(models.Model):
+ PAYMENT_CHOICES = [
+ ('COD', 'Cash on Delivery'),
+ ('ESEWA', 'eSewa'),
+ ('FONEPAY','Fonepay'),
+ ]
+ user = models.ForeignKey(User, on_delete= models.CASCADE)
+ order_id = models.CharField(max_length = 100, unique = True)
+ amount = models.DecimalField(max_digits = 10, decimal_places = 2)
+ location = models.TextField()
+ payment_method = models.CharField(max_length=10, choices = PAYMENT_CHOICES)
+ payment_status = models.CharField(max_length= 20,default ='PENDING')
+ transaction_id = models.CharField(max_length = 100, blank = True, null = True)
+ created_at = models.DateTimeField(auto_now_add =True)
+
+ def __str__(self):
+ return f"Order {self.order_id} - {self.user.username}"
\ No newline at end of file
diff --git a/backend/app/serializer.py b/backend/app/serializer.py
index 5cd92f9..52903a5 100644
--- a/backend/app/serializer.py
+++ b/backend/app/serializer.py
@@ -40,8 +40,8 @@ class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
-
- def __str__(self):
+
+ def __str__(self):
return self.name
class MyTokenPairSerializer(TokenObtainPairSerializer):
diff --git a/backend/app/urls.py b/backend/app/urls.py
index cd1012f..59f0e54 100644
--- a/backend/app/urls.py
+++ b/backend/app/urls.py
@@ -5,12 +5,17 @@
path('products/',views.GetProducts, name="products"),
path('products/category/
',views.GetProductsByCategory, name="products-category"),
path('products/search/',views.GetProductsBySearch, name="products-search"),
- path('login/',views.login, name ="login"),
- path('logout/',views.LogoutView, name ="logout"),
+ path('login/',views.login_view, name ="login"),
+ path('logout/',views.session_logout, name ="logout"),
+ path('get-csrf-token/',views.get_csrf_token, name ="get-csrf-token"),
path('add-to-cart/',views.AddToCart, name = "add-to-cart"),
path('show-cart/',views.ShowCart, name = "show-cart"),
path('plus-cart/',views.PlusCart, name = "plus-cart"),
path('minus-cart/',views.MinusCart, name = "minus-cart"),
path('remove-cart/',views.RemoveCart, name = "remove-cart"),
+ path('checkout/',views.checkout,name="checkout"),
+ # path('esewa/success',views.esewa_success, name="esewa_success"),
+ # path('esewa/failure',views.esewa_failure, name="esewa_failure"),
+
]
\ No newline at end of file
diff --git a/backend/app/views.py b/backend/app/views.py
index c4399f4..4f4d2c0 100644
--- a/backend/app/views.py
+++ b/backend/app/views.py
@@ -9,6 +9,11 @@
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.authtoken.models import Token
+from .esewa_utils import *
+from django.contrib.auth import login as django_login,logout
+from django.http import JsonResponse
+from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
+import uuid
# Create your views here.
class RegisterView(generics.CreateAPIView):
@@ -19,29 +24,25 @@ class RegisterView(generics.CreateAPIView):
def perform_create(self, serializer):
user = serializer.save()
-@api_view(['POST'])
-@permission_classes([AllowAny])
-def login(request):
- serializer = LoginSerializer(data = request.data)
- if serializer.is_valid():
- user = serializer.validated_data['user']
- refresh = RefreshToken.for_user(user)
- access_token = refresh.access_token
- access_token['username'] = user.username
- access_token['phone'] = user.phone
- return Response({
- 'refresh' : str(refresh),
- 'access': str(access_token),
- 'user': {
- 'username': user.username,
- 'phone': user.phone
- }
- }, status = status.HTTP_200_OK)
- else:
- return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
+@api_view(['POST'])
+@permission_classes([AllowAny])
+def login_view(request): # renamed to avoid conflict with login
+ serializer = LoginSerializer(data=request.data, context={'request': request})
+ if serializer.is_valid():
+ user = serializer.validated_data['user']
+ django_login(request, user) # use _request to get the original Django request object
+ return Response({
+ "message": "Login successful",
+ "user": {
+ "username": user.username,
+ "voted": user.phone
+ }
+ })
+
+ return Response(serializer.errors, status=400)
@api_view(['GET'])
def GetProducts(request):
@@ -107,18 +108,14 @@ def ShowCart(request):
@api_view(['POST'])
@permission_classes([IsAuthenticated])
-def LogoutView(request):
- try:
- refresh_token = request.data.get("refresh")
- if not refresh_token:
- return Response({"error": "Refresh token is required"}, status=status.HTTP_400_BAD_REQUEST)
+def session_logout(request):
+ logout(request)
+ return Response({"message": "Logout successful"})
- token = RefreshToken(refresh_token)
- token.blacklist()
+@ensure_csrf_cookie
+def get_csrf_token(request):
+ return JsonResponse({"message": "CSRF cookie set"})
- return Response({"message": "Successfully logged out"}, status=status.HTTP_200_OK)
- except Exception as e:
- return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -185,6 +182,64 @@ def RemoveCart(request):
})
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def checkout(request):
+ user = request.user;
+ location = request.data.get("location")
+ payment_method = request.data.get("payment_method")
+
+ cart_items = Cart.objects.filter(user = user)
+ if not cart_items.exists():
+ return Response({'error': 'Cart is empty'}, status = status.HTTP_400_BAD_REQUEST)
+ total_amount = sum(item.product.price * item.quantity for item in cart_items)
+
+ order_id = f"ORD - {uuid.uuid4().hex[:8]}"
+
+ order = Order.objects.create(
+ user = user,
+ order_id = order_id,
+ amount = total_amount,
+ location = location,
+ payment_method = payment_method,
+ payment_status = 'PENDING'
+ )
+
+ if payment_method == "COD":
+ order.payment_status = 'ACCEPTED'
+ order.save()
+ cart_items.delete()
+
+ return Response({
+ 'status': 'success',
+ 'message': 'Order placed successfully. Payment will be collected on delivery',
+ 'order_id': order_id
+ })
+ elif payment_method == "ESEWA":
+ payment_data = generate_esewa_payment_data(amount = total_amount, order_id=order_id)
+ return Response({'payment_gateway':'esewa',
+ 'payment_url': settings.ESEWA_API_URL,
+ 'payment_data': payment_data,
+ 'method': 'POST'})
+ else:
+ return Response(
+ {'error': 'Invalid payment method'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+
+def esewa_success(request):
+ ref_id = request.GET.get('refId')
+ order_id = request.GET.get('oid')
+ return Response({
+ "value": "Success"
+ })
+
+def failure(request):
+ return Response({
+ "value": "Failure"
+ })
-
+def esewa_test_view(request):
+ return render(request, 'app/esewa/test.html')
diff --git a/backend/backend/settings.py b/backend/backend/settings.py
index 0e2e6a7..f08f399 100644
--- a/backend/backend/settings.py
+++ b/backend/backend/settings.py
@@ -1,3 +1,197 @@
+# """
+# Django settings for backend project.
+
+# Generated by 'django-admin startproject' using Django 5.1.6.
+
+# For more information on this file, see
+# https://docs.djangoproject.com/en/5.1/topics/settings/
+
+# For the full list of settings and their values, see
+# https://docs.djangoproject.com/en/5.1/ref/settings/
+# """
+# from datetime import timedelta
+# from pathlib import Path
+# import os
+# # Build paths inside the project like this: BASE_DIR / 'subdir'.
+# BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# # Quick-start development settings - unsuitable for production
+# # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
+
+# # SECURITY WARNING: keep the secret key used in production secret!
+# SECRET_KEY = 'django-insecure-((s(w$z@8rma5supc9hrp_b_1q_5cn-qj-t0#6@e)=oq^gxr41'
+
+# # SECURITY WARNING: don't run with debug turned on in production!
+# DEBUG = True
+
+# ALLOWED_HOSTS = []
+
+
+# # Application definition
+
+# INSTALLED_APPS = [
+# 'django.contrib.admin',
+# 'django.contrib.auth',
+# 'django.contrib.contenttypes',
+# 'django.contrib.sessions',
+# 'django.contrib.messages',
+# 'django.contrib.staticfiles',
+# 'app',
+# 'corsheaders',
+# 'rest_framework',
+# 'rest_framework_simplejwt',
+# 'rest_framework.authtoken',
+# 'rest_framework_simplejwt.token_blacklist',
+# ]
+
+# REST_FRAMEWORK = {
+# 'DEFAULT_AUTHENTICATION_CLASSES': (
+# 'rest_framework.authentication.SessionAuthentication',
+# # 'rest_framework_simplejwt.authentication.JWTAuthentication',
+# ),
+# # 'DEFAULT_RENDERER_CLASSES':('rest_framework.renderers.JSONRenderer',),
+
+# }
+
+# MIDDLEWARE = [
+# 'django.middleware.security.SecurityMiddleware',
+# 'django.contrib.sessions.middleware.SessionMiddleware',
+# 'corsheaders.middleware.CorsMiddleware',
+# 'django.middleware.common.CommonMiddleware',
+# 'django.middleware.csrf.CsrfViewMiddleware',
+# 'django.contrib.auth.middleware.AuthenticationMiddleware',
+# 'django.contrib.messages.middleware.MessageMiddleware',
+# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+# ]
+
+# ROOT_URLCONF = 'backend.urls'
+
+# TEMPLATES = [
+# {
+# 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+# 'DIRS': [os.path.join(BASE_DIR, 'templates')],
+# 'APP_DIRS': True,
+# 'OPTIONS': {
+# 'context_processors': [
+# 'django.template.context_processors.debug',
+# 'django.template.context_processors.request',
+# 'django.contrib.auth.context_processors.auth',
+# 'django.contrib.messages.context_processors.messages',
+# ],
+# },
+# },
+# ]
+
+# WSGI_APPLICATION = 'backend.wsgi.application'
+
+
+# # Database
+# # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
+
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.postgresql',
+# 'NAME': 'postgres',
+# 'USER': 'postgres.umxbhshsupgsiqvnzduy',
+# 'PASSWORD': 'muSical833201eve',
+# 'HOST': 'aws-0-us-east-2.pooler.supabase.com', # Change to remote DB host if needed
+# 'PORT': '5432', # Default PostgreSQL port
+# }
+# }
+
+
+
+# # Password validation
+# # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
+
+# AUTH_PASSWORD_VALIDATORS = [
+# {
+# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+# },
+# {
+# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+# },
+# {
+# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+# },
+# {
+# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+# },
+# ]
+
+
+# # Internationalization
+# # https://docs.djangoproject.com/en/5.1/topics/i18n/
+
+# LANGUAGE_CODE = 'en-us'
+
+# TIME_ZONE = 'UTC'
+
+# USE_I18N = True
+
+# USE_TZ = True
+
+# AUTH_USER_MODEL = 'app.User'
+
+# # Static files (CSS, JavaScript, Images)
+# # https://docs.djangoproject.com/en/5.1/howto/static-files/
+
+# STATIC_URL = 'static/'
+# MEDIA_URL ='/media/'
+# MEDIA_ROOT = os.path.join(BASE_DIR,'media')
+
+# # Default primary key field type
+# # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
+
+# DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+# SIMPLE_JWT = {
+# "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
+# "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
+# "ROTATE_REFRESH_TOKENS": False,
+# "BLACKLIST_AFTER_ROTATION": False,
+# "UPDATE_LAST_LOGIN": False,
+
+# "AUTH_HEADER_TYPES": ("Bearer",),
+# "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
+# "USER_ID_FIELD": "id",
+# "USER_ID_CLAIM": "user_id",
+# "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
+
+# "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
+# "TOKEN_TYPE_CLAIM": "token_type",
+# "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
+
+# "JTI_CLAIM": "jti",
+
+# }
+# CORS_ALLOW_CREDENTIALS = True
+# CORS_ALLOWED_ORIGINS = [
+# "http://localhost:5173",
+# "http://127.0.0.1:8000",
+# "http://127.0.0.1:3000"
+# ]
+
+# CSRF_TRUSTED_ORIGINS = [
+# "http://localhost:5173",
+# ]
+
+# CORS_ALLOW_CREDENTIALS = True
+# CSRF_COOKIE_HTTPONLY = False
+# SESSION_COOKIE_SECURE = False # True for HTTPS, False for local dev
+# CSRF_COOKIE_SECURE = False
+# SESSION_COOKIE_SAMESITE = 'Lax' # or 'None' if cross-site
+# CSRF_COOKIE_SAMESITE = 'Lax'
+
+
+# ESEWA_MERCHANT_ID = 'EPAYTEST'
+# ESEWA_API_URL = 'https://rc-epay.esewa.com.np/api/epay/main/v2/form'
+# ESEWA_VERIFY_URL = 'https://rc.esewa.com.np/api/epay/transaction/status/'
+# ESEWA_SUCCESS_URL = 'http://127.0.0.1:8000/esewa/success'
+# ESEWA_FAILURE_URL = 'http://127.0.0.1:8000/esewa/failure'
+
"""
Django settings for backend project.
@@ -47,7 +241,8 @@
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
- 'rest_framework_simplejwt.authentication.JWTAuthentication',
+ 'rest_framework.authentication.SessionAuthentication',
+ # 'rest_framework_simplejwt.authentication.JWTAuthentication',
),
# 'DEFAULT_RENDERER_CLASSES':('rest_framework.renderers.JSONRenderer',),
@@ -69,7 +264,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -168,9 +363,25 @@
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
- "http://localhost:8080",
+ "http://localhost:5173",
"http://127.0.0.1:8000",
- "http://localhost:3000",
- "http://localhost:5173",
- "http://127.0.0.1:3000",
-]
\ No newline at end of file
+ "http://127.0.0.1:3000"
+]
+
+CSRF_TRUSTED_ORIGINS = [
+ "http://localhost:5173",
+]
+
+CORS_ALLOW_CREDENTIALS = True
+CSRF_COOKIE_HTTPONLY = False
+SESSION_COOKIE_SECURE = False # True for HTTPS, False for local dev
+CSRF_COOKIE_SECURE = False
+SESSION_COOKIE_SAMESITE = 'Lax' # or 'None' if cross-site
+CSRF_COOKIE_SAMESITE = 'Lax'
+
+
+ESEWA_MERCHANT_ID = 'EPAYTEST'
+ESEWA_API_URL = 'https://rc-epay.esewa.com.np/api/epay/main/v2/form'
+ESEWA_VERIFY_URL = 'https://rc.esewa.com.np/api/epay/transaction/status/'
+ESEWA_SUCCESS_URL = 'http://127.0.0.1:8000/esewa/success'
+ESEWA_FAILURE_URL = 'http://127.0.0.1:8000/esewa/failure'
\ No newline at end of file