From 0a66f6c2186552b9c00dd6619b8a6c35052a5b39 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Mon, 23 Oct 2023 19:00:59 -0400 Subject: [PATCH 001/107] updated auth class --- CCPDController/authentication.py | 56 +++++++++++++++++++++++--------- CCPDController/serializer.py | 12 +++++++ CCPDController/settings.py | 2 +- inventoryController/models.py | 23 ++++++++++--- userController/models.py | 11 +++++-- userController/views.py | 42 +++++++++++++----------- 6 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 CCPDController/serializer.py diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index f859c26..057d0a4 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -11,24 +11,48 @@ # customized authentication class used in settings class JWTAuthentication(TokenAuthentication): - def verifyToken(self, token): - try: - # decode - payload = jwt.decode(token, settings.SECRET_KEY) - uid = ObjectId(payload['id']) + def authenticate(self, request): + print('========================') + print('|| verify token called ||') + print('========================') + + # get auth token in request header + token = request.META.get('HTTP_AUTHORIZATION') + JWTToken = token[7:] + + print(JWTToken) + + if not JWTToken: + print('no tokens found') + return None - print(token) - - # query mongo db for user - user = collection.find_one({'_id': uid}) - # expect errors - except jwt.DecodeError: - raise AuthenticationFailed('Invalid token') - except jwt.ExpiredSignatureError: - raise AuthenticationFailed('Token has expired') + print('========================') + print('|| decode token called ||') + print('========================') + + # decode + payload = jwt.decode(JWTToken, settings.SECRET_KEY, algorithms='HS256') + print(payload) + + + uid = ObjectId(payload['id']) + print(uid) + + # query mongo db for user + user = collection.find_one({'_id': uid}) + + print(user) + + # try: + + + # except jwt.DecodeError or UnicodeError: + # raise AuthenticationFailed('Invalid token') + # except jwt.ExpiredSignatureError: + # raise AuthenticationFailed('Token has expired') # if not activate throw error if not user['userActive']: - raise AuthenticationFailed('User inactive or deleted') - return payload + raise AuthenticationFailed('User inactive') + return True diff --git a/CCPDController/serializer.py b/CCPDController/serializer.py new file mode 100644 index 0000000..d87c29e --- /dev/null +++ b/CCPDController/serializer.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from django.contrib.auth.models import User + +class UserSerializer(serializers.ModelSerializer): + class Meta(object): + model = User + fields = ["email", "password"] + +class StudentSerializer(serializers.ModelSerializer): + class Meta: + model = Student + fields = ["name", "roll", "city"] diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 81d1f84..358a24b 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -81,7 +81,7 @@ # Django rest framework REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'CCPDController.authentication.JWTAuthentication', + # 'CCPDController.authentication.JWTAuthentication', ] } diff --git a/inventoryController/models.py b/inventoryController/models.py index 547c7a7..70eae82 100644 --- a/inventoryController/models.py +++ b/inventoryController/models.py @@ -4,13 +4,29 @@ from jsonfield import JSONField class InventoryItem(models.Model): + CONDITION_CHOISES = [ + ('New', 'New'), + ('Sealed', 'Sealed'), + ('Used', 'Used'), + ('Used Like New', 'Used Like New'), + ('Damaged', 'Damaged'), + ('As Is', 'As Is'), + ] + + PLATFORM_CHOISES = [ + ('Amazon', 'AMAZON'), + ('eBay', 'eBay'), + ('Official Website', 'Official Website'), + ('Other', 'Other') + ] + _id: models.AutoField(primary_key=True) time: models.CharField(max_length=30) - sku: models.CharField(max_length=10) - itemCondition: models.CharField(max_length=10) + sku: models.IntegerField(max_length=10) + itemCondition: models.CharField(max_length=14, choices=CONDITION_CHOISES) comment: models.TextField() link: models.TextField() - platform: models.CharField(max_length=10) + platform: models.CharField(max_length=16, choices=PLATFORM_CHOISES) shelfLocation: models.CharField(max_length=4) amount: models.IntegerField(max_length=3) owner: models.CharField(max_length=32) @@ -26,7 +42,6 @@ def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocat self.shelfLocation = shelfLocation self.amount = amount self.owner = owner - # self.images = images # return inventory sku def __str__(self) -> str: diff --git a/userController/models.py b/userController/models.py index c84151e..5a5fb0e 100644 --- a/userController/models.py +++ b/userController/models.py @@ -2,11 +2,16 @@ from django.core.validators import MinLengthValidator class User(models.Model): + ROLE_CHOISES = [ + ('Admin', 'Admin'), + ('QAPersonal', 'QAPersonal'), + ] + _id: models.AutoField(primary_key=True) - name: models.CharField(max_length=20, validators=[MinLengthValidator(3, 'Need to input a longer name')]) + name: models.CharField(max_length=20, validators=[MinLengthValidator(3, 'Name Invalid')]) email: models.EmailField(max_length=45, validators=[MinLengthValidator(8, 'Email Invalid')]) - password: models.CharField(max_length=45, validators=[MinLengthValidator(8, 'password Invalid')]) - role: models.CharField(max_length=15, validators=[MinLengthValidator(8, 'role Invalid')]) + password: models.CharField(max_length=45, validators=[MinLengthValidator(8, 'Password Invalid')]) + role: models.CharField(max_length=12, validators=[MinLengthValidator(4, 'Role Invalid')], choices=ROLE_CHOISES) registrationDate: models.CharField(max_length=30, validators=[MinLengthValidator(8,'Registration date invalid')]) userActive: models.BooleanField() diff --git a/userController/views.py b/userController/views.py index 5c83bf3..d9c48aa 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,11 +5,10 @@ from django.views.decorators.csrf import csrf_exempt from datetime import date, datetime, timedelta from bson.objectid import ObjectId -import bson.json_util as json_util from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny -from rest_framework.exceptions import AuthenticationFailed +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from CCPDController.authentication import JWTAuthentication from rest_framework import status from rest_framework.response import Response from userController.models import User @@ -22,32 +21,32 @@ def checkBody(body): return HttpResponse('Invalid Email') elif len(body['password']) < 8 or len(body['password']) > 45: return HttpResponse('Invalid Password') - + # pymongo db = get_db_client() collection = db['User'] @api_view(['POST']) -# @permission_classes([AllowAny]) +@permission_classes([AllowAny]) def login(request): - body = json.loads(request.body.decode('utf-8')) + body = decodeJSON(request.body) # sanitize email = sanitizeEmail(body['email']) password = sanitizePassword(body['password']) - if email == False or password == False: - return Response(AuthenticationFailed()) + if email is False or password is False: + return Response('Invalid Login Information', status=status.HTTP_400_BAD_REQUEST) # check if user exist user = collection.find_one({'email': email, 'password': password}) - - # if not found or user inactive, return failed - if user is None or user['userActive'] is False: - return AuthenticationFailed() + if user is None: + return Response('Login Failed', status=status.HTTP_404_NOT_FOUND) + if user['userActive'] is False: + return Response('User Inactive', status=status.HTTP_401_UNAUTHORIZED) # return jwt token to user payload = { - 'id': json_util.dumps(user['_id']), + 'id': str(ObjectId(user['_id'])), 'exp': datetime.utcnow() + timedelta(days=14), 'iat': datetime.utcnow() } @@ -55,17 +54,23 @@ def login(request): # construct tokent and return it token = jwt.encode(payload, settings.SECRET_KEY) return Response(token, status=status.HTTP_200_OK) - + @api_view(['GET']) +@authentication_classes([JWTAuthentication]) @permission_classes([IsAuthenticated]) def getUserById(request): body = decodeJSON(request.body) - # convert to BSON - uid = ObjectId(body['_id']) + try: + # convert to BSON + uid = ObjectId(body['_id']) + except: + return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) # query db for user res = collection.find_one({'_id': uid}) + if not res: + return Response('User Not Found', status=status.HTTP_404_NOT_FOUND) # construct user object resUser = User( @@ -78,8 +83,7 @@ def getUserById(request): ) # return as json object - return Response(json.dumps(resUser.__dict__), status=status.HTTP_200_OK) - + return Response(resUser.__dict__, status=status.HTTP_200_OK) @csrf_exempt @api_view(['POST']) From c2929870ff5d5f5bd29beefbb33ba60c20e879b7 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 24 Oct 2023 18:51:06 -0400 Subject: [PATCH 002/107] updated auth function --- CCPDController/authentication.py | 47 ++++++++-------- CCPDController/permissions.py | 43 +++++++++++++++ inventoryController/views.py | 93 +++++++++++++++----------------- package-lock.json | 6 --- userController/views.py | 28 +++++----- 5 files changed, 124 insertions(+), 93 deletions(-) create mode 100644 CCPDController/permissions.py delete mode 100644 package-lock.json diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 057d0a4..fdd92d9 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -4,6 +4,7 @@ from django.conf import settings from rest_framework.exceptions import AuthenticationFailed from CCPDController.utils import get_db_client +from userController.models import User # pymongo db = get_db_client() @@ -11,48 +12,46 @@ # customized authentication class used in settings class JWTAuthentication(TokenAuthentication): + + keyword = 'Bearer' + model = User + + # run query against database to verify user info by querying user id + async def authenticate_credentials(self, id): + + # query mongo db for user + try: + uid = ObjectId(id) + except: + raise AuthenticationFailed('Invalid ID') + user = await collection.find_one({'_id': uid}) + print(user) + if not user['userActive']: + raise AuthenticationFailed('User Inactive') + return (user, None) + + # called everytime when accessing restricted router def authenticate(self, request): print('========================') print('|| verify token called ||') print('========================') - - # get auth token in request header + # get auth token in request header and concat token = request.META.get('HTTP_AUTHORIZATION') JWTToken = token[7:] - print(JWTToken) if not JWTToken: print('no tokens found') - return None - + raise AuthenticationFailed('Token Not Found') - print('========================') - print('|| decode token called ||') - print('========================') - # decode payload = jwt.decode(JWTToken, settings.SECRET_KEY, algorithms='HS256') print(payload) - - uid = ObjectId(payload['id']) - print(uid) - - # query mongo db for user - user = collection.find_one({'_id': uid}) - - print(user) - # try: - # except jwt.DecodeError or UnicodeError: # raise AuthenticationFailed('Invalid token') # except jwt.ExpiredSignatureError: # raise AuthenticationFailed('Token has expired') - - # if not activate throw error - if not user['userActive']: - raise AuthenticationFailed('User inactive') - return True + return self.authenticate_credentials(payload['id']) diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py new file mode 100644 index 0000000..73e9308 --- /dev/null +++ b/CCPDController/permissions.py @@ -0,0 +1,43 @@ +from rest_framework import permissions +from rest_framework.permissions import BasePermission +from CCPDController.utils import get_db_client + +# pymongo +db = get_db_client() +collection = db['User'] + +# QA personal permission +class QAPermission(permissions.BasePermission): + message = 'Permission Denied' + + def has_permission(self, request, view): + + # do mongo query to see user role + return super().has_permission(request, view) + + +# determine if user have permission to access inventory object +class IsInventoryOwnerPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + # always allow GET HEAD OPTION method + if request.method in permissions.SAFE_METHODS: + return True + + # query db see if object owned by user + return obj.owner == request.user + +# admin permission +class IsAdminPermission(permissions.BasePermission): + def has_permission(self, request, view): + # query database to see if user is admin + return super().has_permission(request, view) + + +# user blocked by IP +class BlockedPermission(permissions.BasePermission): + message = 'You Are Blocked From Our Service' + + def has_permission(self, request, view): + ipAddress = request.META['REMOTE_ADDR'] + # query database for blocked ip address + # blocked = diff --git a/inventoryController/views.py b/inventoryController/views.py index 37ba0bd..e16f6ea 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -11,63 +11,58 @@ # query param sku for inventory db row @api_view(['GET']) -def getInventoryBySku(request): +async def getInventoryBySku(request): body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) if sku: - res = collection.find_one({'sku': sku}) + res = await collection.find_one({'sku': sku}) return HttpResponse(res) else: - return HttpResponse('Invalid SKU') - - + return HttpResponse('Invalid SKU') -# get all inventory by QA personal -def getInventoryByOwner(request): - if request.method == 'GET': - body = decodeJSON(request.body) - - collection.find_one({ - - }) - print (request) +# get all inventory by QA personal +@api_view(['GET']) +async def getInventoryByOwner(request): + body = decodeJSON(request.body) - + await collection.find_one({ + + }) + print (request) # create single inventory Q&A record @csrf_exempt -def createInventory(request): - if request.method == 'PUT': - body = decodeJSON(request.body) - sku = sanitizeSku(body['sku']) - comment = removeStr(body['comment']) - - - # if sku exist return error - collection.find_one({'sku': body['sku']}) - - # construct new inventory - newInventory = InventoryItem( - time=str(ctime(time())), - sku=sku, - itemCondition=body['itemCondition'], - comment=body['comment'], - link=body['link'], - platform=body['platform'], - shelfLocation=body['shelfLocation'], - amount=body['amount'], - owner=body['owner'], - # images=body["images"] if body["images"]==None else None - ) - - # pymongo need dict or bson object - res = collection.insert_one(newInventory.__dict__) - return HttpResponse(newInventory) +@api_view(['PUT']) +async def createInventory(request): + body = decodeJSON(request.body) + sku = sanitizeSku(body['sku']) + comment = removeStr(body['comment']) + # if sku exist return error + await collection.find_one({'sku': body['sku']}) + + # construct new inventory + newInventory = InventoryItem( + time=str(ctime(time())), + sku=sku, + itemCondition=body['itemCondition'], + comment=body['comment'], + link=body['link'], + platform=body['platform'], + shelfLocation=body['shelfLocation'], + amount=body['amount'], + owner=body['owner'], + # images=body["images"] if body["images"]==None else None + ) + + # pymongo need dict or bson object + res = collection.insert_one(newInventory.__dict__) + return HttpResponse(newInventory) # query param sku and body of new inventory info -def updateInventoryById(request): +@api_view(['POST']) +async def updateInventoryById(request): if request.method == 'POST': body = decodeJSON(request.body) @@ -75,9 +70,9 @@ def updateInventoryById(request): print(request) # delete inventory by sku -def deleteInventoryBySku(request): - if request.method == 'DELETE': - body = decodeJSON(request.body) - - # delete inventory by sku - collection.find_one({ 'sku': body['sku'] }) +@api_view(['DELETE']) +async def deleteInventoryBySku(request): + body = decodeJSON(request.body) + + # delete inventory by sku + await collection.find_one({ 'sku': body['sku'] }) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1d62620..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "ccpd-django-controller", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/userController/views.py b/userController/views.py index d9c48aa..f3a3892 100644 --- a/userController/views.py +++ b/userController/views.py @@ -28,7 +28,7 @@ def checkBody(body): @api_view(['POST']) @permission_classes([AllowAny]) -def login(request): +async def login(request): body = decodeJSON(request.body) # sanitize @@ -38,13 +38,13 @@ def login(request): return Response('Invalid Login Information', status=status.HTTP_400_BAD_REQUEST) # check if user exist - user = collection.find_one({'email': email, 'password': password}) + user = await collection.find_one({'email': email, 'password': password}) if user is None: return Response('Login Failed', status=status.HTTP_404_NOT_FOUND) if user['userActive'] is False: return Response('User Inactive', status=status.HTTP_401_UNAUTHORIZED) - # return jwt token to user + # construct payload payload = { 'id': str(ObjectId(user['_id'])), 'exp': datetime.utcnow() + timedelta(days=14), @@ -58,7 +58,7 @@ def login(request): @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAuthenticated]) -def getUserById(request): +async def getUserById(request): body = decodeJSON(request.body) try: @@ -68,7 +68,7 @@ def getUserById(request): return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) # query db for user - res = collection.find_one({'_id': uid}) + res = await collection.find_one({'_id': uid}) if not res: return Response('User Not Found', status=status.HTTP_404_NOT_FOUND) @@ -87,14 +87,14 @@ def getUserById(request): @csrf_exempt @api_view(['POST']) -def registerUser(request): +async def registerUser(request): body = decodeJSON(request.body) # check if body is valid checkBody(body) # check if email exist in database - res = collection.find_one({ 'email': body['email'] }) + res = await collection.find_one({ 'email': body['email'] }) # check for existing email if res: @@ -111,7 +111,7 @@ def registerUser(request): ) # insert user into db - res = collection.insert_one(newUser.__dict__) + res = await collection.insert_one(newUser.__dict__) # return the registration result if res: @@ -121,18 +121,18 @@ def registerUser(request): # delete user by id @api_view(['DELETE']) -def deleteUserById(request): +async def deleteUserById(request): body = decodeJSON(request.body) # convert to BSON uid = ObjectId(body['_id']) # query db for user - res = collection.find_one({'_id': uid}) + res = await collection.find_one({'_id': uid}) # if found, delete it if res : - res = collection.delete_one({'_id': uid}) + res = await collection.delete_one({'_id': uid}) return HttpResponse('User Deleted') else: return HttpResponse('User Not Found') @@ -142,14 +142,14 @@ def deleteUserById(request): # admin password have to be set in mongo manually @api_view(['PUT']) @csrf_exempt -def updatePasswordById(request): +async def updatePasswordById(request): body = decodeJSON(request.body) # convert to BSON uid = ObjectId(body['_id']) # query db for user - res = collection.find_one({ + res = await collection.find_one({ '_id': uid, 'role': 'QAPersonal' }) @@ -160,7 +160,7 @@ def updatePasswordById(request): # if found, change its pass word if res : - collection.update_one( + await collection.update_one( { '_id': uid }, { '$set': {'password': body['password']} } ) From dce14891b3013ad6989cd752cd105c544840abbc Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 25 Oct 2023 18:57:10 -0400 Subject: [PATCH 003/107] updated permission and auth --- CCPDController/authentication.py | 59 ++++++++++++-------------- CCPDController/permissions.py | 41 ++++++++++-------- CCPDController/settings.py | 1 + CCPDController/utils.py | 26 +++++++----- userController/views.py | 72 +++++++++++++++++--------------- 5 files changed, 104 insertions(+), 95 deletions(-) diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index fdd92d9..8a3df8d 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -11,47 +11,42 @@ collection = db['User'] # customized authentication class used in settings -class JWTAuthentication(TokenAuthentication): - - keyword = 'Bearer' - model = User - +class JWTAuthentication(TokenAuthentication): # run query against database to verify user info by querying user id - async def authenticate_credentials(self, id): - - # query mongo db for user + def authenticate_credentials(self, id): + # if id cannot convert into ObjectId, throw error try: uid = ObjectId(id) except: raise AuthenticationFailed('Invalid ID') - user = await collection.find_one({'_id': uid}) - print(user) + + # get only id status and role + user = collection.find_one({'_id': uid}, {'userActive': 1, 'role': 1}) + + # check user activation status if not user['userActive']: raise AuthenticationFailed('User Inactive') - return (user, None) + + # return type have to be tuple + return (user, user['role']) # called everytime when accessing restricted router def authenticate(self, request): - print('========================') - print('|| verify token called ||') - print('========================') - # get auth token in request header and concat - token = request.META.get('HTTP_AUTHORIZATION') - JWTToken = token[7:] - print(JWTToken) - - if not JWTToken: - print('no tokens found') - raise AuthenticationFailed('Token Not Found') + try: + # get auth token in request header and concat + token = request.META.get('HTTP_AUTHORIZATION') + + # remove space and auth type + JWTToken = token[7:] + + # if no token throw not found + if not JWTToken or len(JWTToken) < 1: + raise AuthenticationFailed('Token Not Found') - # decode - payload = jwt.decode(JWTToken, settings.SECRET_KEY, algorithms='HS256') - print(payload) - - # try: - - # except jwt.DecodeError or UnicodeError: - # raise AuthenticationFailed('Invalid token') - # except jwt.ExpiredSignatureError: - # raise AuthenticationFailed('Token has expired') + # decode jwt and retrive user id + payload = jwt.decode(JWTToken, settings.SECRET_KEY, algorithms='HS256') + except jwt.DecodeError or UnicodeError: + raise AuthenticationFailed('Invalid token') + except jwt.ExpiredSignatureError: + raise AuthenticationFailed('Token has expired') return self.authenticate_credentials(payload['id']) diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py index 73e9308..459086a 100644 --- a/CCPDController/permissions.py +++ b/CCPDController/permissions.py @@ -7,33 +7,38 @@ collection = db['User'] # QA personal permission -class QAPermission(permissions.BasePermission): - message = 'Permission Denied' - - def has_permission(self, request, view): +class IsQAPermission(permissions.BasePermission): + message = 'Permission Denied, QAPersonal Only' + def has_permission(self, request, view): + print(" HAS PERMISSION :: ") + print(request.user) + print(request.auth) - # do mongo query to see user role - return super().has_permission(request, view) + # mongo db query + # grant if user is qa personal and user is active + if request.auth == 'QAPersonal' and request.user['userActive'] == True: + return True - # determine if user have permission to access inventory object class IsInventoryOwnerPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - # always allow GET HEAD OPTION method - if request.method in permissions.SAFE_METHODS: - return True - - # query db see if object owned by user - return obj.owner == request.user - -# admin permission + message = 'Permission Denied, Only Owner Have Access' + def has_object_permission(self, request, view, obj): + # always allow GET HEAD OPTION method + if request.method in permissions.SAFE_METHODS: + return True + + # query db see if object owned by user + return obj.owner == request.user + +# admin permission class IsAdminPermission(permissions.BasePermission): + message = 'Permission Denied, Admin Only!' + def has_permission(self, request, view): # query database to see if user is admin return super().has_permission(request, view) - -# user blocked by IP +# user blocked by IP black list class BlockedPermission(permissions.BasePermission): message = 'You Are Blocked From Our Service' diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 358a24b..a9644ce 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -80,6 +80,7 @@ # Django rest framework REST_FRAMEWORK = { + # default auth class for all routes 'DEFAULT_AUTHENTICATION_CLASSES': [ # 'CCPDController.authentication.JWTAuthentication', ] diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 2f281bf..e959ef6 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -16,11 +16,15 @@ def get_db_client(): # decode body from json to object decodeJSON = lambda body : json.loads(body.decode('utf-8'))\ - -# check if obj is type t -def checkT(obj, t): - if type(obj) is not t: - return False + +# check if body contains valid user information +def checkBody(body): + if len(body['name']) < 3 or len(body['name']) > 40: + return HttpResponse('Invalid Name') + elif len(body['email']) < 6 or len(body['email']) > 45 or '@' not in body['email']: + return HttpResponse('Invalid Email') + elif len(body['password']) < 8 or len(body['password']) > 45: + return HttpResponse('Invalid Password') # check input length def checkLen(input, minLen, maxLen): @@ -38,7 +42,7 @@ def removeStr(input): # skuy can be from 3 chars to 40 chars def sanitizeSku(sku): # type check - if checkT(sku, int): + if not isinstance(sku, int): return False # len check @@ -49,7 +53,7 @@ def sanitizeSku(sku): # name can be from 3 chars to 40 chars def sanitizeName(name): # type check - if checkT(name, str): + if not isinstance(name, str): return False # remove danger chars @@ -63,7 +67,7 @@ def sanitizeName(name): # email can be from 7 chars to 40 chars def sanitizeEmail(email): # type and format check - if checkT(email, str) or '@' not in email: + if not isinstance(email, str) or '@' not in email: return False # len check @@ -73,7 +77,7 @@ def sanitizeEmail(email): # password can be from 8 chars to 40 chars def sanitizePassword(password): - if checkT(password, str): + if not isinstance(password, str): return False if len(password) < 8 or len(password) > 40: return False @@ -83,8 +87,8 @@ def sanitizePassword(password): def sanitizePlatform(platform): if platform not in ['Amazon', 'eBay', 'Official Website', 'Other']: return False - + # shelf location sanitize def sanitizeShelfLocation(shelfLocation): - if type(shelfLocation) is not str: + if not isinstance(shelfLocation, str): return False diff --git a/userController/views.py b/userController/views.py index f3a3892..9d3aa2e 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,22 +5,14 @@ from django.views.decorators.csrf import csrf_exempt from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from CCPDController.permissions import IsQAPermission from CCPDController.authentication import JWTAuthentication from rest_framework import status from rest_framework.response import Response from userController.models import User - -# check if body contains valid user information -def checkBody(body): - if len(body['name']) < 3 or len(body['name']) > 40: - return HttpResponse('Invalid Name') - elif len(body['email']) < 6 or len(body['email']) > 45 or '@' not in body['email']: - return HttpResponse('Invalid Email') - elif len(body['password']) < 8 or len(body['password']) > 45: - return HttpResponse('Invalid Password') # pymongo db = get_db_client() @@ -28,26 +20,34 @@ def checkBody(body): @api_view(['POST']) @permission_classes([AllowAny]) -async def login(request): +def login(request): body = decodeJSON(request.body) # sanitize email = sanitizeEmail(body['email']) password = sanitizePassword(body['password']) - if email is False or password is False: + if email == False or password == False: return Response('Invalid Login Information', status=status.HTTP_400_BAD_REQUEST) # check if user exist - user = await collection.find_one({'email': email, 'password': password}) - if user is None: + # only retrive user status and role + user = collection.find_one({ + 'email': email, + 'password': password + }, { 'userActive': 1, 'role': 1 }) + + print(user) + + # check user status + if user == None: return Response('Login Failed', status=status.HTTP_404_NOT_FOUND) - if user['userActive'] is False: + if user['userActive'] == False: return Response('User Inactive', status=status.HTTP_401_UNAUTHORIZED) # construct payload payload = { 'id': str(ObjectId(user['_id'])), - 'exp': datetime.utcnow() + timedelta(days=14), + 'exp': datetime.utcnow() + timedelta(seconds=30), 'iat': datetime.utcnow() } @@ -57,8 +57,8 @@ async def login(request): @api_view(['GET']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsAuthenticated]) -async def getUserById(request): +@permission_classes([IsQAPermission]) +def getUserById(request): body = decodeJSON(request.body) try: @@ -68,7 +68,7 @@ async def getUserById(request): return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) # query db for user - res = await collection.find_one({'_id': uid}) + res = collection.find_one({'_id': uid}) if not res: return Response('User Not Found', status=status.HTTP_404_NOT_FOUND) @@ -87,14 +87,14 @@ async def getUserById(request): @csrf_exempt @api_view(['POST']) -async def registerUser(request): +def registerUser(request): body = decodeJSON(request.body) # check if body is valid checkBody(body) # check if email exist in database - res = await collection.find_one({ 'email': body['email'] }) + res = collection.find_one({ 'email': body['email'] }) # check for existing email if res: @@ -111,7 +111,7 @@ async def registerUser(request): ) # insert user into db - res = await collection.insert_one(newUser.__dict__) + res = collection.insert_one(newUser.__dict__) # return the registration result if res: @@ -121,46 +121,50 @@ async def registerUser(request): # delete user by id @api_view(['DELETE']) -async def deleteUserById(request): +def deleteUserById(request): body = decodeJSON(request.body) # convert to BSON uid = ObjectId(body['_id']) # query db for user - res = await collection.find_one({'_id': uid}) + res = collection.find_one({'_id': uid}) # if found, delete it if res : - res = await collection.delete_one({'_id': uid}) + res = collection.delete_one({'_id': uid}) return HttpResponse('User Deleted') else: return HttpResponse('User Not Found') - -# update user password + +# need object level permission # qa personals can update their own password # admin password have to be set in mongo manually @api_view(['PUT']) -@csrf_exempt -async def updatePasswordById(request): +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission]) +def updatePasswordById(request): body = decodeJSON(request.body) - # convert to BSON - uid = ObjectId(body['_id']) + # if failed to convert to BSON response 401 + try: + uid = ObjectId(body['_id']) + except: + return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) # query db for user - res = await collection.find_one({ + res = collection.find_one({ '_id': uid, 'role': 'QAPersonal' }) # check if password is valid - if len(body['password']) < 8 or len(body['password']) > 45: + if not sanitizePassword(body['password']): return HttpResponse('Invalid Password') # if found, change its pass word if res : - await collection.update_one( + collection.update_one( { '_id': uid }, { '$set': {'password': body['password']} } ) From ea0d1e0cad5e50c4c7d3cc943bcbc4f9327486f9 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 26 Oct 2023 19:04:32 -0400 Subject: [PATCH 004/107] added admin controller --- adminController/__init__.py | 0 adminController/admin.py | 3 ++ adminController/apps.py | 6 +++ adminController/migrations/__init__.py | 0 adminController/models.py | 3 ++ adminController/tests.py | 3 ++ adminController/urls.py | 7 ++++ adminController/views.py | 55 +++++++++++++++++++++++++ userController/urls.py | 1 - userController/views.py | 57 +++++++++----------------- 10 files changed, 97 insertions(+), 38 deletions(-) create mode 100644 adminController/__init__.py create mode 100644 adminController/admin.py create mode 100644 adminController/apps.py create mode 100644 adminController/migrations/__init__.py create mode 100644 adminController/models.py create mode 100644 adminController/tests.py create mode 100644 adminController/urls.py create mode 100644 adminController/views.py diff --git a/adminController/__init__.py b/adminController/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adminController/admin.py b/adminController/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/adminController/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/adminController/apps.py b/adminController/apps.py new file mode 100644 index 0000000..f290d7f --- /dev/null +++ b/adminController/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AdmincontrollerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'adminController' diff --git a/adminController/migrations/__init__.py b/adminController/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adminController/models.py b/adminController/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/adminController/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/adminController/tests.py b/adminController/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/adminController/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/adminController/urls.py b/adminController/urls.py new file mode 100644 index 0000000..5cf3469 --- /dev/null +++ b/adminController/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +# define all routes +urlpatterns = [ + path('login', views.login, name="login"), +] diff --git a/adminController/views.py b/adminController/views.py new file mode 100644 index 0000000..2842082 --- /dev/null +++ b/adminController/views.py @@ -0,0 +1,55 @@ +from django.shortcuts import render +from bson.objectid import ObjectId +from rest_framework import status +from rest_framework.response import Response +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from CCPDController.permissions import IsQAPermission +from CCPDController.authentication import JWTAuthentication +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword + +# pymongo +db = get_db_client() +user_collection = db['User'] + +# delete user by id +@api_view(['DELETE']) +def deleteUserById(request): + body = decodeJSON(request.body) + + # convert to BSON + try: + uid = ObjectId(body['_id']) + except: + return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) + + # query db for user + res = user_collection.find_one({'_id': uid}) + + # if found, delete it + if res : + user_collection.delete_one({'_id': uid}) + return Response('User Deleted', status.HTTP_200_OK) + else: + return Response('User Not Found', status.HTTP_404_NOT_FOUND) + +@api_view(['POST']) +def setUserActive(request): + body = decodeJSON(request.body) + + # convert to BSON + try: + uid = ObjectId(body['_id']) + except: + return Response('Invalid User ID') + + # query db for user + res = user_collection.find_one({'_id': uid}) + + # if found, switch user active to false + if res : + res = user_collection.update_one({'_id': uid}) + return Response('User Deleted') + else: + return Response('User Not Found') + diff --git a/userController/urls.py b/userController/urls.py index b9cd401..d6c6bb8 100644 --- a/userController/urls.py +++ b/userController/urls.py @@ -6,6 +6,5 @@ path('login', views.login, name="login"), path("registerUser", views.registerUser, name="registerUser"), path("getUserById", views.getUserById, name="getUserById"), - path("deleteUserById", views.deleteUserById, name="deleteUserById"), path("updatePasswordById", views.updatePasswordById, name="updatePasswordById") ] diff --git a/userController/views.py b/userController/views.py index 9d3aa2e..7afa193 100644 --- a/userController/views.py +++ b/userController/views.py @@ -6,10 +6,10 @@ from datetime import date, datetime, timedelta from bson.objectid import ObjectId from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny from CCPDController.permissions import IsQAPermission from CCPDController.authentication import JWTAuthentication +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny from rest_framework import status from rest_framework.response import Response from userController.models import User @@ -27,7 +27,7 @@ def login(request): email = sanitizeEmail(body['email']) password = sanitizePassword(body['password']) if email == False or password == False: - return Response('Invalid Login Information', status=status.HTTP_400_BAD_REQUEST) + return Response('Invalid Login Information', status.HTTP_400_BAD_REQUEST) # check if user exist # only retrive user status and role @@ -36,14 +36,12 @@ def login(request): 'password': password }, { 'userActive': 1, 'role': 1 }) - print(user) - # check user status if user == None: - return Response('Login Failed', status=status.HTTP_404_NOT_FOUND) + return Response('Login Failed', status.HTTP_404_NOT_FOUND) if user['userActive'] == False: - return Response('User Inactive', status=status.HTTP_401_UNAUTHORIZED) - + return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) + # construct payload payload = { 'id': str(ObjectId(user['_id'])), @@ -53,7 +51,7 @@ def login(request): # construct tokent and return it token = jwt.encode(payload, settings.SECRET_KEY) - return Response(token, status=status.HTTP_200_OK) + return Response(token, status.HTTP_200_OK) @api_view(['GET']) @authentication_classes([JWTAuthentication]) @@ -65,18 +63,20 @@ def getUserById(request): # convert to BSON uid = ObjectId(body['_id']) except: - return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) + return Response('User ID Invalid:', status.HTTP_401_UNAUTHORIZED) # query db for user - res = collection.find_one({'_id': uid}) + res = collection.find_one( + { '_id': uid }, + { 'name': 1, 'email': 1, 'role': 1, 'registrationDate': 1, 'userActive': 1 } + ) if not res: - return Response('User Not Found', status=status.HTTP_404_NOT_FOUND) + return Response('User Not Found', status.HTTP_404_NOT_FOUND) # construct user object resUser = User( name=res['name'], email=res['email'], - password='***', role=res['role'], registrationDate=res['registrationDate'], userActive=res['userActive'] @@ -98,7 +98,7 @@ def registerUser(request): # check for existing email if res: - return HttpResponse('Email already existed!') + return Response('Email already existed!') # construct user newUser = User( @@ -115,27 +115,10 @@ def registerUser(request): # return the registration result if res: - return HttpResponse(True) + return Response(True) else: - return HttpResponse(False) + return Response(False) -# delete user by id -@api_view(['DELETE']) -def deleteUserById(request): - body = decodeJSON(request.body) - - # convert to BSON - uid = ObjectId(body['_id']) - - # query db for user - res = collection.find_one({'_id': uid}) - - # if found, delete it - if res : - res = collection.delete_one({'_id': uid}) - return HttpResponse('User Deleted') - else: - return HttpResponse('User Not Found') # need object level permission # qa personals can update their own password @@ -150,7 +133,7 @@ def updatePasswordById(request): try: uid = ObjectId(body['_id']) except: - return Response('User ID Invalid:', status=status.HTTP_401_UNAUTHORIZED) + return Response('User ID Invalid:', status.HTTP_401_UNAUTHORIZED) # query db for user res = collection.find_one({ @@ -160,7 +143,7 @@ def updatePasswordById(request): # check if password is valid if not sanitizePassword(body['password']): - return HttpResponse('Invalid Password') + return Response('Invalid Password') # if found, change its pass word if res : @@ -168,7 +151,7 @@ def updatePasswordById(request): { '_id': uid }, { '$set': {'password': body['password']} } ) - return HttpResponse('Password Updated') + return Response('Password Updated') else: - return HttpResponse('User Not Found') + return Response('User Not Found') From 0f4bf054aecddeff46629c1e2a078477eec75a52 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 27 Oct 2023 18:58:50 -0400 Subject: [PATCH 005/107] updated utilities and controllers --- CCPDController/authentication.py | 2 + CCPDController/permissions.py | 32 +++++------- CCPDController/settings.py | 6 +++ CCPDController/urls.py | 1 + CCPDController/utils.py | 27 ++++++++--- adminController/urls.py | 4 +- adminController/views.py | 69 ++++++++++++++++++++++---- userController/urls.py | 2 +- userController/views.py | 83 +++++++++++++++++--------------- 9 files changed, 148 insertions(+), 78 deletions(-) diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 8a3df8d..0090976 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -35,6 +35,8 @@ def authenticate(self, request): try: # get auth token in request header and concat token = request.META.get('HTTP_AUTHORIZATION') + if not token: + raise AuthenticationFailed('Token Not Found') # remove space and auth type JWTToken = token[7:] diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py index 459086a..56a492f 100644 --- a/CCPDController/permissions.py +++ b/CCPDController/permissions.py @@ -6,37 +6,29 @@ db = get_db_client() collection = db['User'] +# user object will be pass into here from authentication +# auth = ROLE +# user = { +# '_id': ObjectId(xxx), +# 'userActive': xxx, +# 'role': xxx +# } + # QA personal permission class IsQAPermission(permissions.BasePermission): - message = 'Permission Denied, QAPersonal Only' - def has_permission(self, request, view): - print(" HAS PERMISSION :: ") - print(request.user) - print(request.auth) - + message = 'Permission Denied, QAPersonal Only!' + def has_permission(self, request, view): # mongo db query # grant if user is qa personal and user is active if request.auth == 'QAPersonal' and request.user['userActive'] == True: return True -# determine if user have permission to access inventory object -class IsInventoryOwnerPermission(permissions.BasePermission): - message = 'Permission Denied, Only Owner Have Access' - def has_object_permission(self, request, view, obj): - # always allow GET HEAD OPTION method - if request.method in permissions.SAFE_METHODS: - return True - - # query db see if object owned by user - return obj.owner == request.user - # admin permission class IsAdminPermission(permissions.BasePermission): message = 'Permission Denied, Admin Only!' - def has_permission(self, request, view): - # query database to see if user is admin - return super().has_permission(request, view) + if request.auth == 'Admin' and bool(request.user['userActive']) == True : + return True # user blocked by IP black list class BlockedPermission(permissions.BasePermission): diff --git a/CCPDController/settings.py b/CCPDController/settings.py index a9644ce..0fc8e47 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -24,6 +24,7 @@ INSTALLED_APPS = [ 'rest_framework', 'corsheaders', + 'django_user_agents', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -37,6 +38,7 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', + 'django_user_agents.middleware.UserAgentMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -86,6 +88,10 @@ ] } +# Name of cache backend to cache user agents. If it not specified default +# cache alias will be used. Set to `None` to disable caching. +USER_AGENTS_CACHE = None + # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', diff --git a/CCPDController/urls.py b/CCPDController/urls.py index 0f9ff28..1988855 100644 --- a/CCPDController/urls.py +++ b/CCPDController/urls.py @@ -6,4 +6,5 @@ path('imageController/', include('imageController.urls')), path('inventoryController/', include('inventoryController.urls')), path('userController/', include('userController.urls')), + path('adminController/', include('adminController.urls')), ] diff --git a/CCPDController/utils.py b/CCPDController/utils.py index e959ef6..ad11692 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -17,17 +17,28 @@ def get_db_client(): # decode body from json to object decodeJSON = lambda body : json.loads(body.decode('utf-8'))\ +# limit variables +max_name = 40 +min_name = 3 +max_email = 45 +min_email = 6 +max_password = 70 +min_password = 8 +min_sku = 4 +max_sku = 7 + # check if body contains valid user information def checkBody(body): - if len(body['name']) < 3 or len(body['name']) > 40: + if inRange(body['name'], min_name, min_email): return HttpResponse('Invalid Name') - elif len(body['email']) < 6 or len(body['email']) > 45 or '@' not in body['email']: + elif len(body['email']) < min_email or len(body['email']) > max_email or '@' not in body['email']: return HttpResponse('Invalid Email') - elif len(body['password']) < 8 or len(body['password']) > 45: + elif len(body['password']) < min_password or len(body['password']) > max_password: return HttpResponse('Invalid Password') # check input length -def checkLen(input, minLen, maxLen): +# if input is in range return true else return false +def inRange(input, minLen, maxLen): if len(str(input)) < minLen or len(str(input)) > maxLen: return False else: @@ -46,7 +57,7 @@ def sanitizeSku(sku): return False # len check - if not checkLen(sku, 4, 7): + if not inRange(sku, min_sku, max_sku): return False return sku @@ -60,7 +71,7 @@ def sanitizeName(name): clean_name = removeStr(name) # len check - if len(clean_name) < 3 or len(clean_name) > 40: + if not inRange(clean_name, min_name, max_name): return False return clean_name @@ -71,7 +82,7 @@ def sanitizeEmail(email): return False # len check - if len(email) < 7 or len(email) > 40: + if not inRange(email, min_email, max_email): return False return email @@ -79,7 +90,7 @@ def sanitizeEmail(email): def sanitizePassword(password): if not isinstance(password, str): return False - if len(password) < 8 or len(password) > 40: + if not inRange(password, min_password, max_password): return False return password diff --git a/adminController/urls.py b/adminController/urls.py index 5cf3469..d133954 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -3,5 +3,7 @@ # define all routes urlpatterns = [ - path('login', views.login, name="login"), + path('deleteUserById', views.deleteUserById, name="deleteUserById"), + path('setUserActive', views.setUserActive, name="setUserActive"), + path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), ] diff --git a/adminController/views.py b/adminController/views.py index 2842082..4a425da 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -1,10 +1,11 @@ +import uuid from django.shortcuts import render from bson.objectid import ObjectId from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny -from CCPDController.permissions import IsQAPermission +from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword @@ -13,6 +14,7 @@ user_collection = db['User'] # delete user by id +# '_id': xxxx, @api_view(['DELETE']) def deleteUserById(request): body = decodeJSON(request.body) @@ -30,10 +32,13 @@ def deleteUserById(request): if res : user_collection.delete_one({'_id': uid}) return Response('User Deleted', status.HTTP_200_OK) - else: - return Response('User Not Found', status.HTTP_404_NOT_FOUND) + return Response('User Not Found', status.HTTP_404_NOT_FOUND) -@api_view(['POST']) + +# set any user status to be active or disabled +# '_id': xxxx, +# 'password': xxxx +@api_view(['PUT']) def setUserActive(request): body = decodeJSON(request.body) @@ -41,15 +46,61 @@ def setUserActive(request): try: uid = ObjectId(body['_id']) except: - return Response('Invalid User ID') + return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) # query db for user res = user_collection.find_one({'_id': uid}) # if found, switch user active to false if res : - res = user_collection.update_one({'_id': uid}) - return Response('User Deleted') - else: - return Response('User Not Found') + res = user_collection.update_one( + { '_id': uid }, + { '$set': {'userActive': body['userActive']} } + ) + if res: + return Response('Updated User Activation Status', status.HTTP_200_OK) + return Response('User Not Found') + +# admin generate invitation code for newly hired QA personal to join +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def issueInvitationCode(request): + + # generate a uuid for invitation code + inviteCode = uuid.uuid4() + print(inviteCode) + + return Response('Invitation Code Created: '.join(inviteCode), status.HTTP_200_OK) + +# update anyones password by id +# '_id': xxxx, +# 'password': xxxx +@api_view(['PUT']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def updatePasswordById(request): + body = decodeJSON(request.body) + + # if failed to convert to BSON response 401 + try: + uid = ObjectId(body['_id']) + except: + return Response('User ID Invalid:', status.HTTP_400_BAD_REQUEST) + + # query db for user + res = user_collection.find_one({ '_id': uid }) + + # check if password is valid + if not sanitizePassword(body['password']): + return Response('Invalid Password', status.HTTP_400_BAD_REQUEST) + + # if found, change its pass word + if res: + user_collection.update_one( + { '_id': uid }, + { '$set': {'password': body['password']} } + ) + return Response('Password Updated', status.HTTP_200_OK) + return Response('User Not Found', status.HTTP_404_NOT_FOUND) diff --git a/userController/urls.py b/userController/urls.py index d6c6bb8..d410a83 100644 --- a/userController/urls.py +++ b/userController/urls.py @@ -6,5 +6,5 @@ path('login', views.login, name="login"), path("registerUser", views.registerUser, name="registerUser"), path("getUserById", views.getUserById, name="getUserById"), - path("updatePasswordById", views.updatePasswordById, name="updatePasswordById") + path("changeOwnPassword", views.changeOwnPassword, name="changeOwnPassword") ] diff --git a/userController/views.py b/userController/views.py index 7afa193..21bf50d 100644 --- a/userController/views.py +++ b/userController/views.py @@ -18,11 +18,13 @@ db = get_db_client() collection = db['User'] +# login any user and issue jwt +# _id: xxx @api_view(['POST']) @permission_classes([AllowAny]) def login(request): body = decodeJSON(request.body) - + # sanitize email = sanitizeEmail(body['email']) password = sanitizePassword(body['password']) @@ -32,7 +34,7 @@ def login(request): # check if user exist # only retrive user status and role user = collection.find_one({ - 'email': email, + 'email': email, 'password': password }, { 'userActive': 1, 'role': 1 }) @@ -45,7 +47,7 @@ def login(request): # construct payload payload = { 'id': str(ObjectId(user['_id'])), - 'exp': datetime.utcnow() + timedelta(seconds=30), + 'exp': datetime.utcnow() + timedelta(hours=1), 'iat': datetime.utcnow() } @@ -53,6 +55,8 @@ def login(request): token = jwt.encode(payload, settings.SECRET_KEY) return Response(token, status.HTTP_200_OK) +# get user information without password +# _id: xxx @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission]) @@ -70,7 +74,7 @@ def getUserById(request): { '_id': uid }, { 'name': 1, 'email': 1, 'role': 1, 'registrationDate': 1, 'userActive': 1 } ) - if not res: + if not res or not res['userActive']: return Response('User Not Found', status.HTTP_404_NOT_FOUND) # construct user object @@ -78,6 +82,7 @@ def getUserById(request): name=res['name'], email=res['email'], role=res['role'], + password=None, registrationDate=res['registrationDate'], userActive=res['userActive'] ) @@ -85,20 +90,28 @@ def getUserById(request): # return as json object return Response(resUser.__dict__, status=status.HTTP_200_OK) +# user registration +# name: xxx +# email: xxx +# password: xxx +# inviationCode: xxx @csrf_exempt @api_view(['POST']) def registerUser(request): body = decodeJSON(request.body) - - # check if body is valid - checkBody(body) + checkBody(body) # sanitization + + # prevent user_agent that is not mobile or tablet from registration + # print(request.user_agent.is_mobile) + # print(request.user_agent.is_tablet) + # print(request.user_agent.is_touch_capable) + # print(request.user_agent.is_pc) + # print(request.user_agent) # check if email exist in database res = collection.find_one({ 'email': body['email'] }) - - # check for existing email if res: - return Response('Email already existed!') + return Response('Email already existed!', status.HTTP_400_BAD_REQUEST) # construct user newUser = User( @@ -113,45 +126,37 @@ def registerUser(request): # insert user into db res = collection.insert_one(newUser.__dict__) - # return the registration result if res: - return Response(True) - else: - return Response(False) + return Response('Registration Successful', status.HTTP_200_OK) + return Response('Registration Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) -# need object level permission -# qa personals can update their own password -# admin password have to be set in mongo manually +# QA personal change own password +# _id: xxx +# newPassword: xxxx @api_view(['PUT']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission]) -def updatePasswordById(request): +def changeOwnPassword(request): body = decodeJSON(request.body) - # if failed to convert to BSON response 401 + # convert to BSON try: uid = ObjectId(body['_id']) + password = sanitizePassword(body['newPassword']) except: - return Response('User ID Invalid:', status.HTTP_401_UNAUTHORIZED) + return Response('User ID or Password Invalid:', status.HTTP_401_UNAUTHORIZED) - # query db for user - res = collection.find_one({ - '_id': uid, - 'role': 'QAPersonal' - }) - - # check if password is valid - if not sanitizePassword(body['password']): - return Response('Invalid Password') + # query for uid and role to be QA personal and update + res = collection.update_one( + { + '_id': uid, + 'role': 'QAPersonal' + }, + { '$set': {'password': password} } + ) - # if found, change its pass word - if res : - collection.update_one( - { '_id': uid }, - { '$set': {'password': body['password']} } - ) - return Response('Password Updated') - else: - return Response('User Not Found') - + if res: + return Response('Password Updated', status.HTTP_200_OK) + return Response('Cannot Update Password', status.HTTP_500_INTERNAL_SERVER_ERROR) + \ No newline at end of file From af245f61bd3e94f3e81131f59bfd874c900f0524 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 1 Nov 2023 18:59:49 -0400 Subject: [PATCH 006/107] updated authentication method --- CCPDController/authentication.py | 47 +++++++++++++++++++++--------- CCPDController/settings.py | 6 +++- userController/urls.py | 3 +- userController/views.py | 50 ++++++++++++++++++++------------ 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 0090976..f5f58b2 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -1,15 +1,22 @@ import jwt -from rest_framework.authentication import get_authorization_header, TokenAuthentication +from rest_framework.authentication import CSRFCheck, TokenAuthentication from bson.objectid import ObjectId from django.conf import settings -from rest_framework.exceptions import AuthenticationFailed +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from CCPDController.utils import get_db_client -from userController.models import User # pymongo db = get_db_client() collection = db['User'] +# check for csrf token in request +def enforce_csrf(request): + check = CSRFCheck(request) + check.process_request(request) + reason = check.process_view(request, None, (), {}) + if reason: + raise PermissionDenied('CSRF Failed: %s' % reason) + # customized authentication class used in settings class JWTAuthentication(TokenAuthentication): # run query against database to verify user info by querying user id @@ -33,22 +40,34 @@ def authenticate_credentials(self, id): # called everytime when accessing restricted router def authenticate(self, request): try: - # get auth token in request header and concat - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - raise AuthenticationFailed('Token Not Found') + print(request.__dict__) + # # get auth token in request header and concat + # token = request.META.get('HTTP_AUTHORIZATION') + # if not token: + # raise AuthenticationFailed('Token Not Found') - # remove space and auth type - JWTToken = token[7:] + # # remove space and auth type + # JWTToken = token[7:] + + # # if no token throw not found + # if not JWTToken or len(JWTToken) < 1: + # raise AuthenticationFailed('Token Not Found') + + # get token from cookies + raw_token = request.COOKIES.get('token') or None + + if not raw_token: + raise AuthenticationFailed('No token provided') - # if no token throw not found - if not JWTToken or len(JWTToken) < 1: - raise AuthenticationFailed('Token Not Found') - # decode jwt and retrive user id - payload = jwt.decode(JWTToken, settings.SECRET_KEY, algorithms='HS256') + payload = jwt.decode(raw_token, settings.SECRET_KEY, algorithms='HS256') + except jwt.DecodeError or UnicodeError: raise AuthenticationFailed('Invalid token') except jwt.ExpiredSignatureError: raise AuthenticationFailed('Token has expired') + + # TODO + # check the reason why csrf token cannot be fetch on logout + # enforce_csrf(request) return self.authenticate_credentials(payload['id']) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 0fc8e47..9223a52 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -33,7 +33,8 @@ 'django.contrib.staticfiles', 'imageController', 'inventoryController', - 'userController' + 'userController', + 'adminController' ] MIDDLEWARE = [ @@ -68,6 +69,9 @@ WSGI_APPLICATION = 'CCPDController.wsgi.application' +# for http only cookies +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = True # CORS stuff CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ diff --git a/userController/urls.py b/userController/urls.py index d410a83..2a6008a 100644 --- a/userController/urls.py +++ b/userController/urls.py @@ -6,5 +6,6 @@ path('login', views.login, name="login"), path("registerUser", views.registerUser, name="registerUser"), path("getUserById", views.getUserById, name="getUserById"), - path("changeOwnPassword", views.changeOwnPassword, name="changeOwnPassword") + path("changeOwnPassword", views.changeOwnPassword, name="changeOwnPassword"), + path("logout", views.logout, name="logout"), ] diff --git a/userController/views.py b/userController/views.py index 21bf50d..dcbd75f 100644 --- a/userController/views.py +++ b/userController/views.py @@ -2,7 +2,8 @@ import json from django.conf import settings from django.shortcuts import HttpResponse -from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.csrf import csrf_exempt, csrf_protect +from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody @@ -13,13 +14,17 @@ from rest_framework import status from rest_framework.response import Response from userController.models import User - + # pymongo db = get_db_client() collection = db['User'] +# jwt token expiring time +expire_days = 1 + # login any user and issue jwt # _id: xxx +@csrf_protect @api_view(['POST']) @permission_classes([AllowAny]) def login(request): @@ -41,19 +46,25 @@ def login(request): # check user status if user == None: return Response('Login Failed', status.HTTP_404_NOT_FOUND) - if user['userActive'] == False: + if bool(user['userActive']) == False: return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) # construct payload payload = { 'id': str(ObjectId(user['_id'])), - 'exp': datetime.utcnow() + timedelta(hours=1), + 'exp': datetime.utcnow() + timedelta(days=expire_days), 'iat': datetime.utcnow() } # construct tokent and return it - token = jwt.encode(payload, settings.SECRET_KEY) - return Response(token, status.HTTP_200_OK) + token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + + # construct response store jwt token in http only cookie + response = Response('Login Success', status.HTTP_200_OK) + response.set_cookie('token', token, httponly=True) + response.set_cookie('csrftoken', get_token(request), httponly=True) + # response["X-CSRFToken"] = get_token(request) + return response # get user information without password # _id: xxx @@ -74,7 +85,7 @@ def getUserById(request): { '_id': uid }, { 'name': 1, 'email': 1, 'role': 1, 'registrationDate': 1, 'userActive': 1 } ) - if not res or not res['userActive']: + if not res or not bool(res['userActive']): return Response('User Not Found', status.HTTP_404_NOT_FOUND) # construct user object @@ -84,7 +95,7 @@ def getUserById(request): role=res['role'], password=None, registrationDate=res['registrationDate'], - userActive=res['userActive'] + userActive=bool(res['userActive']) ) # return as json object @@ -101,13 +112,6 @@ def registerUser(request): body = decodeJSON(request.body) checkBody(body) # sanitization - # prevent user_agent that is not mobile or tablet from registration - # print(request.user_agent.is_mobile) - # print(request.user_agent.is_tablet) - # print(request.user_agent.is_touch_capable) - # print(request.user_agent.is_pc) - # print(request.user_agent) - # check if email exist in database res = collection.find_one({ 'email': body['email'] }) if res: @@ -130,10 +134,9 @@ def registerUser(request): return Response('Registration Successful', status.HTTP_200_OK) return Response('Registration Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) - # QA personal change own password # _id: xxx -# newPassword: xxxx +# newPassword: xxx @api_view(['PUT']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission]) @@ -159,4 +162,15 @@ def changeOwnPassword(request): if res: return Response('Password Updated', status.HTTP_200_OK) return Response('Cannot Update Password', status.HTTP_500_INTERNAL_SERVER_ERROR) - \ No newline at end of file + +@csrf_protect +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission]) +def logout(request): + # construct response + response = Response('User Logout', status.HTTP_200_OK) + # delete jwt token and csrf token + response.delete_cookie('token') + response.delete_cookie('csrftoken') + return response \ No newline at end of file From 6126184dcb929b64eb73de7bfcfa4e96a9904111 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 2 Nov 2023 19:02:08 -0400 Subject: [PATCH 007/107] updated authentication flow --- CCPDController/authentication.py | 16 +----- CCPDController/settings.py | 13 +++-- CCPDController/utils.py | 18 +++---- adminController/views.py | 32 ++++++------ inventoryController/urls.py | 4 +- inventoryController/views.py | 74 ++++++++++++++++++++++------ userController/urls.py | 1 + userController/views.py | 84 +++++++++++++++++++++----------- 8 files changed, 153 insertions(+), 89 deletions(-) diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index f5f58b2..9187133 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -40,22 +40,8 @@ def authenticate_credentials(self, id): # called everytime when accessing restricted router def authenticate(self, request): try: - print(request.__dict__) - # # get auth token in request header and concat - # token = request.META.get('HTTP_AUTHORIZATION') - # if not token: - # raise AuthenticationFailed('Token Not Found') - - # # remove space and auth type - # JWTToken = token[7:] - - # # if no token throw not found - # if not JWTToken or len(JWTToken) < 1: - # raise AuthenticationFailed('Token Not Found') - - # get token from cookies + # check for http-only cookies raw_token = request.COOKIES.get('token') or None - if not raw_token: raise AuthenticationFailed('No token provided') diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 9223a52..02e1c09 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -71,17 +71,22 @@ # for http only cookies SESSION_COOKIE_HTTPONLY = True -CSRF_COOKIE_HTTPONLY = True -# CORS stuff + +# cors stuff +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ "http://localhost:8100", - "http://127.0.0.1:8100" + "http://127.0.0.1:8100", + "http://192.168.2.62:8100" ] +# csrf stuff +CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ "http://127.0.0.1:8100", - "http://localhost:8100" + "http://localhost:8100", + "http://192.168.2.62:8100" ] # Django rest framework diff --git a/CCPDController/utils.py b/CCPDController/utils.py index ad11692..ef40c9e 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -2,7 +2,7 @@ import json import jwt from django.conf import settings -from django.shortcuts import HttpResponse +from rest_framework.response import Response from rest_framework import exceptions from pymongo import MongoClient from dotenv import load_dotenv @@ -24,17 +24,18 @@ def get_db_client(): min_email = 6 max_password = 70 min_password = 8 -min_sku = 4 max_sku = 7 +min_sku = 4 # check if body contains valid user information def checkBody(body): - if inRange(body['name'], min_name, min_email): - return HttpResponse('Invalid Name') - elif len(body['email']) < min_email or len(body['email']) > max_email or '@' not in body['email']: - return HttpResponse('Invalid Email') - elif len(body['password']) < min_password or len(body['password']) > max_password: - return HttpResponse('Invalid Password') + if not inRange(body['name'], min_name, max_name): + return False + elif not inRange(body['email'], min_email, max_email) or '@' not in body['email']: + return False + elif not inRange(body['password'], min_password, max_password): + return False + return body # check input length # if input is in range return true else return false @@ -47,7 +48,6 @@ def inRange(input, minLen, maxLen): # sanitize mongodb strings def removeStr(input): input.replace('$', '') - input.replace('.', '') return input # skuy can be from 3 chars to 40 chars diff --git a/adminController/views.py b/adminController/views.py index 4a425da..c7c4adb 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -14,13 +14,14 @@ user_collection = db['User'] # delete user by id -# '_id': xxxx, +# _id: string @api_view(['DELETE']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) def deleteUserById(request): - body = decodeJSON(request.body) - - # convert to BSON try: + # convert to BSON + body = decodeJSON(request.body) uid = ObjectId(body['_id']) except: return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) @@ -36,14 +37,16 @@ def deleteUserById(request): # set any user status to be active or disabled -# '_id': xxxx, -# 'password': xxxx +# _id: string, +# password: string +# userActive: bool @api_view(['PUT']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) def setUserActive(request): - body = decodeJSON(request.body) - - # convert to BSON try: + # convert to BSON + body = decodeJSON(request.body) uid = ObjectId(body['_id']) except: return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) @@ -63,7 +66,7 @@ def setUserActive(request): # admin generate invitation code for newly hired QA personal to join -@api_view(['POST']) +@api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def issueInvitationCode(request): @@ -75,16 +78,15 @@ def issueInvitationCode(request): return Response('Invitation Code Created: '.join(inviteCode), status.HTTP_200_OK) # update anyones password by id -# '_id': xxxx, -# 'password': xxxx +# _id: string +# newpassword: string @api_view(['PUT']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def updatePasswordById(request): - body = decodeJSON(request.body) - - # if failed to convert to BSON response 401 try: + # if failed to convert to BSON response 401 + body = decodeJSON(request.body) uid = ObjectId(body['_id']) except: return Response('User ID Invalid:', status.HTTP_400_BAD_REQUEST) diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 5cda09c..9f1220f 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -6,6 +6,6 @@ path("getInventoryBySku", views.getInventoryBySku, name="getInventoryBySku"), path("createInventory", views.createInventory, name="createInventory"), path("deleteInventoryBySku", views.deleteInventoryBySku, name="deleteInventoryBySku"), - path("updateInventoryById", views.updateInventoryById, name="updateInventoryById"), - path("getInventoryByOwner", views.getInventoryByOwner, name="getInventoryByOwner") + path("updateInventoryBySku", views.updateInventoryBySku, name="updateInventoryBySku"), + path("getInventoryByOwnerId", views.getInventoryByOwnerId, name="getInventoryByOwnerId") ] diff --git a/inventoryController/views.py b/inventoryController/views.py index e16f6ea..12806b3 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -1,9 +1,15 @@ from time import time, ctime from django.shortcuts import HttpResponse +from django.views.decorators.csrf import csrf_exempt from inventoryController.models import InventoryItem from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, sanitizeName, removeStr -from django.views.decorators.csrf import csrf_exempt -from rest_framework.decorators import api_view +from CCPDController.permissions import IsQAPermission, IsAdminPermission +from CCPDController.authentication import JWTAuthentication +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework import status +from bson.objectid import ObjectId # pymongo db = get_db_client() @@ -11,29 +17,44 @@ # query param sku for inventory db row @api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission, IsAdminPermission]) async def getInventoryBySku(request): body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) if sku: res = await collection.find_one({'sku': sku}) - return HttpResponse(res) + return Response(res) else: - return HttpResponse('Invalid SKU') + return Response('Invalid SKU') # get all inventory by QA personal @api_view(['GET']) -async def getInventoryByOwner(request): +def getInventoryByOwnerId(request): body = decodeJSON(request.body) + print(body['_id']) - await collection.find_one({ - - }) - print (request) + try: + ownerId = ObjectId(body['_id']) + except: + return Response('Invalid Id', status.HTTP_400_BAD_REQUEST) + + + # need a fix + arr = [] + for inventory in collection.find({ 'owner': str(ownerId) }): + arr.append(inventory) + + print(arr) + + return Response(arr, status.HTTP_200_OK) # create single inventory Q&A record @csrf_exempt @api_view(['PUT']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission, IsAdminPermission]) async def createInventory(request): body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) @@ -53,24 +74,45 @@ async def createInventory(request): shelfLocation=body['shelfLocation'], amount=body['amount'], owner=body['owner'], - # images=body["images"] if body["images"]==None else None ) # pymongo need dict or bson object res = collection.insert_one(newInventory.__dict__) - return HttpResponse(newInventory) + return Response(newInventory) # query param sku and body of new inventory info +# sku @api_view(['POST']) -async def updateInventoryById(request): - if request.method == 'POST': +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission, IsAdminPermission]) +def updateInventoryBySku(request): + try: + # convert to object id body = decodeJSON(request.body) - - # query if inventory exist - print(request) + sku = sanitizeSku(body['sku']) + except: + return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) + + + # update inventory + res = collection.update_one( + { + 'sku': sku, + }, + { '$set': + { + 'password': + '' + } + } + ) + # delete inventory by sku +# admin only @api_view(['DELETE']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) async def deleteInventoryBySku(request): body = decodeJSON(request.body) diff --git a/userController/urls.py b/userController/urls.py index 2a6008a..f1c63d9 100644 --- a/userController/urls.py +++ b/userController/urls.py @@ -3,6 +3,7 @@ # define all routes urlpatterns = [ + path('checkToken', views.checkToken, name="checkToken"), path('login', views.login, name="login"), path("registerUser", views.registerUser, name="registerUser"), path("getUserById", views.getUserById, name="getUserById"), diff --git a/userController/views.py b/userController/views.py index dcbd75f..76680fb 100644 --- a/userController/views.py +++ b/userController/views.py @@ -1,5 +1,4 @@ import jwt -import json from django.conf import settings from django.shortcuts import HttpResponse from django.views.decorators.csrf import csrf_exempt, csrf_protect @@ -7,10 +6,11 @@ from datetime import date, datetime, timedelta from bson.objectid import ObjectId from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody -from CCPDController.permissions import IsQAPermission +from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from rest_framework.permissions import AllowAny +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from rest_framework import status from rest_framework.response import Response from userController.models import User @@ -22,6 +22,27 @@ # jwt token expiring time expire_days = 1 +# will be called every time on open app +@csrf_protect +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission | IsAdminPermission]) +def checkToken(request): + # get token + token = request.COOKIES.get('token') + + # decode and return user id + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms='HS256') + except jwt.DecodeError or UnicodeError: + raise AuthenticationFailed('Invalid token') + except jwt.ExpiredSignatureError: + raise AuthenticationFailed('Token has expired') + + if token: + return Response(payload['id'], status.HTTP_200_OK) + return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) + # login any user and issue jwt # _id: xxx @csrf_protect @@ -49,21 +70,23 @@ def login(request): if bool(user['userActive']) == False: return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) - # construct payload - payload = { - 'id': str(ObjectId(user['_id'])), - 'exp': datetime.utcnow() + timedelta(days=expire_days), - 'iat': datetime.utcnow() - } - - # construct tokent and return it - token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + try: + # construct payload + payload = { + 'id': str(ObjectId(user['_id'])), + 'exp': datetime.utcnow() + timedelta(days=expire_days), + 'iat': datetime.utcnow() + } + + # construct tokent and return it + token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + except: + return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) # construct response store jwt token in http only cookie - response = Response('Login Success', status.HTTP_200_OK) + response = Response(str(ObjectId(user['_id'])), status.HTTP_200_OK) response.set_cookie('token', token, httponly=True) response.set_cookie('csrftoken', get_token(request), httponly=True) - # response["X-CSRFToken"] = get_token(request) return response # get user information without password @@ -72,13 +95,12 @@ def login(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission]) def getUserById(request): - body = decodeJSON(request.body) - try: # convert to BSON + body = decodeJSON(request.body) uid = ObjectId(body['_id']) except: - return Response('User ID Invalid:', status.HTTP_401_UNAUTHORIZED) + return Response('Invalid User ID', status.HTTP_401_UNAUTHORIZED) # query db for user res = collection.find_one( @@ -105,12 +127,16 @@ def getUserById(request): # name: xxx # email: xxx # password: xxx -# inviationCode: xxx -@csrf_exempt +# inviationCode: xxx (pending) +@csrf_protect @api_view(['POST']) def registerUser(request): - body = decodeJSON(request.body) - checkBody(body) # sanitization + # sanitization + body = checkBody(decodeJSON(request.body)) + if not body: + return Response('Invalid registration info!', status.HTTP_400_BAD_REQUEST) + + # implement invitation code later # check if email exist in database res = collection.find_one({ 'email': body['email'] }) @@ -141,10 +167,9 @@ def registerUser(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission]) def changeOwnPassword(request): - body = decodeJSON(request.body) - - # convert to BSON try: + # convert to BSON + body = decodeJSON(request.body) uid = ObjectId(body['_id']) password = sanitizePassword(body['newPassword']) except: @@ -166,11 +191,14 @@ def changeOwnPassword(request): @csrf_protect @api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission]) +@permission_classes([IsAdminPermission | IsQAPermission]) def logout(request): # construct response response = Response('User Logout', status.HTTP_200_OK) - # delete jwt token and csrf token - response.delete_cookie('token') - response.delete_cookie('csrftoken') + try: + # delete jwt token and csrf token + response.delete_cookie('token') + response.delete_cookie('csrftoken') + except: + return Response('Token Not Found', status.HTTP_404_NOT_FOUND) return response \ No newline at end of file From 463d8f1fe59057feb14ebdcf3e91650a9f31ec4b Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 4 Nov 2023 19:00:12 -0400 Subject: [PATCH 008/107] fixed controllers --- inventoryController/views.py | 125 +++++++++++++++++++++++++---------- userController/views.py | 14 +++- 2 files changed, 100 insertions(+), 39 deletions(-) diff --git a/inventoryController/views.py b/inventoryController/views.py index 12806b3..edbebea 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -10,58 +10,70 @@ from rest_framework.response import Response from rest_framework import status from bson.objectid import ObjectId +import json # pymongo db = get_db_client() collection = db['Inventory'] # query param sku for inventory db row -@api_view(['GET']) +@api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission, IsAdminPermission]) -async def getInventoryBySku(request): - body = decodeJSON(request.body) - sku = sanitizeSku(body['sku']) +@permission_classes([IsQAPermission | IsAdminPermission]) +def getInventoryBySku(request): + try: + body = decodeJSON(request.body) + sku = sanitizeSku(body['sku']) + except: + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) - if sku: - res = await collection.find_one({'sku': sku}) - return Response(res) - else: - return Response('Invalid SKU') + if not sku: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + res = collection.find_one({'sku': sku}, {'_id': 0}) + + if not res: + return Response(sku, status.HTTP_404_NOT_FOUND) + return Response(res, status.HTTP_200_OK) # get all inventory by QA personal -@api_view(['GET']) +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission | IsAdminPermission]) def getInventoryByOwnerId(request): - body = decodeJSON(request.body) - print(body['_id']) + print(request.body) + try: + body = decodeJSON(request.body) + except: + return Response('Invalid Body',status.HTTP_400_BAD_REQUEST) + try: - ownerId = ObjectId(body['_id']) + ownerId = str(ObjectId(body['id'])) except: return Response('Invalid Id', status.HTTP_400_BAD_REQUEST) - - # need a fix + # return all inventory from owner in array arr = [] - for inventory in collection.find({ 'owner': str(ownerId) }): + for inventory in collection.find({ 'owner': ownerId }): + inventory['_id'] = str(inventory['_id']) arr.append(inventory) - print(arr) - return Response(arr, status.HTTP_200_OK) # create single inventory Q&A record -@csrf_exempt @api_view(['PUT']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission, IsAdminPermission]) -async def createInventory(request): +@permission_classes([IsQAPermission | IsAdminPermission]) +def createInventory(request): body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) comment = removeStr(body['comment']) # if sku exist return error - await collection.find_one({'sku': body['sku']}) + inv = collection.find_one({'sku': body['sku']}) + if inv: + return Response('SKU Already Existed') # construct new inventory newInventory = InventoryItem( @@ -81,29 +93,68 @@ async def createInventory(request): return Response(newInventory) # query param sku and body of new inventory info -# sku +# sku: string +# newInventory: Inventory +""" +{ + sku: xxxxx, + newInv: { + sku, + itemCondition, + comment, + link, + platform, + shelfLocation, + amount + } +} +""" @api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission, IsAdminPermission]) +@permission_classes([IsQAPermission | IsAdminPermission]) def updateInventoryBySku(request): try: # convert to object id body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) + newInv = body['newInventory'] except: return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) + # grab existing inventory + oldInv = collection.find_one({ 'sku': sku }) + if not oldInv: + return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) + try: + # construct new inventory + newInventory = InventoryItem( + time = str(ctime(time())), + sku = newInv['sku'] if newInv['sku'] is not None else oldInv['sku'], + itemCondition = newInv['itemCondition'] if newInv['itemCondition'] is not None else oldInv['itemCondition'], + comment = newInv['comment'] if newInv['comment'] is not None else oldInv['comment'], + link = newInv['link'] if newInv['link'] is not None else oldInv['link'], + platform = newInv['platform'] if newInv['platform'] is not None else oldInv['platform'], + shelfLocation = newInv['shelfLocation'] if newInv['shelfLocation'] is not None else oldInv['shelfLocation'], + amount = newInv['amount'] if newInv['amount'] is not None else oldInv['amount'] + ) + except: + return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) + # update inventory res = collection.update_one( + { 'sku': sku }, { - 'sku': sku, - }, - { '$set': + '$set': { - 'password': - '' - } + 'sku': newInventory.sku, + 'itemCondition': newInventory.itemCondition, + 'comment': newInventory.comment, + 'link': newInventory.link, + 'platform': newInventory.platform, + 'shelfLocation': newInventory.shelfLocation, + 'amount': newInventory.amount + } } ) @@ -113,8 +164,10 @@ def updateInventoryBySku(request): @api_view(['DELETE']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) -async def deleteInventoryBySku(request): - body = decodeJSON(request.body) - - # delete inventory by sku - await collection.find_one({ 'sku': body['sku'] }) +def deleteInventoryBySku(request): + try: + body = decodeJSON(request.body) + sanitizeSku(body['sku']) + except: + # delete inventory by sku + collection.find_one({ 'sku': body['sku'] }) diff --git a/userController/views.py b/userController/views.py index 76680fb..d679fa8 100644 --- a/userController/views.py +++ b/userController/views.py @@ -40,7 +40,9 @@ def checkToken(request): raise AuthenticationFailed('Token has expired') if token: - return Response(payload['id'], status.HTTP_200_OK) + user = collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1}) + if user: + return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name']}, status.HTTP_200_OK) return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) # login any user and issue jwt @@ -62,7 +64,7 @@ def login(request): user = collection.find_one({ 'email': email, 'password': password - }, { 'userActive': 1, 'role': 1 }) + }, { 'userActive': 1, 'role': 1, 'name': 1 }) # check user status if user == None: @@ -83,8 +85,14 @@ def login(request): except: return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) + # return the id and name + info = { + 'id': str(ObjectId(user['_id'])), + 'name': user['name'] + } + # construct response store jwt token in http only cookie - response = Response(str(ObjectId(user['_id'])), status.HTTP_200_OK) + response = Response(info, status.HTTP_200_OK) response.set_cookie('token', token, httponly=True) response.set_cookie('csrftoken', get_token(request), httponly=True) return response From d07316b3bd79045906aae52f5159f0bcc649c15a Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 8 Nov 2023 18:48:20 -0500 Subject: [PATCH 009/107] added admin only auth route --- adminController/urls.py | 3 ++ adminController/views.py | 98 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/adminController/urls.py b/adminController/urls.py index d133954..ba310ae 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -3,7 +3,10 @@ # define all routes urlpatterns = [ + path('checkAdminToken', views.checkAdminToken, name='checkAdminToken'), + path('adminLogin', views.adminLogin, name='adminLogin'), path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('setUserActive', views.setUserActive, name="setUserActive"), path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), + path('getAllInventory', views.getAllInventory, name="getAllInventory") ] diff --git a/adminController/views.py b/adminController/views.py index c7c4adb..a3915f9 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -1,10 +1,17 @@ +import jwt import uuid +import json +from django.conf import settings from django.shortcuts import render +from django.views.decorators.csrf import csrf_protect +from django.middleware.csrf import get_token from bson.objectid import ObjectId +from datetime import date, datetime, timedelta from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword @@ -12,6 +19,86 @@ # pymongo db = get_db_client() user_collection = db['User'] +inventory_collection = db['Inventory'] + +# admin jwt token expiring time +admin_expire_days = 90 + +@csrf_protect +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def checkAdminToken(request): + # get token + token = request.COOKIES.get('token') + + # decode and return user id + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms='HS256') + except jwt.DecodeError or UnicodeError: + raise AuthenticationFailed('Invalid token') + except jwt.ExpiredSignatureError: + raise AuthenticationFailed('Token has expired') + + if token: + user = user_collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1}) + if user: + return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name']}, status.HTTP_200_OK) + return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) + +# login any user and issue jwt +@csrf_protect +@api_view(['POST']) +@permission_classes([AllowAny]) +def adminLogin(request): + body = decodeJSON(request.body) + + # sanitize + email = sanitizeEmail(body['email']) + password = sanitizePassword(body['password']) + if email == False or password == False: + return Response('Invalid Login Information', status.HTTP_400_BAD_REQUEST) + + # check if user exist + # only retrive user status and role + user = user_collection.find_one({ + 'email': email, + 'password': password + }, { 'userActive': 1, 'role': 1, 'name': 1 }) + + # check user status + if not user: + return Response('Login Failed', status.HTTP_404_NOT_FOUND) + if bool(user['userActive']) == False: + return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) + if (user['role'] != 'Admin'): + return Response('Permission Denied', status.HTTP_403_FORBIDDEN) + + try: + # construct payload + payload = { + 'id': str(ObjectId(user['_id'])), + 'exp': datetime.utcnow() + timedelta(days=admin_expire_days), + 'iat': datetime.utcnow() + } + + # construct tokent and return it + token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + except: + return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) + + # return the id and name + info = { + 'id': str(ObjectId(user['_id'])), + 'name': user['name'] + } + + # construct response store jwt token in http only cookie + response = Response(info, status.HTTP_200_OK) + response.set_cookie('token', token, httponly=True) + response.set_cookie('csrftoken', get_token(request), httponly=True) + return response + # delete user by id # _id: string @@ -106,3 +193,14 @@ def updatePasswordById(request): ) return Response('Password Updated', status.HTTP_200_OK) return Response('User Not Found', status.HTTP_404_NOT_FOUND) + + +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getAllInventory(request): + inv = [] + for item in inventory_collection.find({}, {'_id': 0}): + inv.append(item) + + return Response(inv, status.HTTP_200_OK) \ No newline at end of file From 733a496fde87bdf2d66f8aa852ed84d81f77f3e1 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 11 Nov 2023 19:02:15 -0500 Subject: [PATCH 010/107] added image uploading function --- imageController/urls.py | 6 +- imageController/views.py | 104 ++++++++++++++++------------------- inventoryController/views.py | 26 ++++++--- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/imageController/urls.py b/imageController/urls.py index a207413..c71a3c9 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -3,9 +3,7 @@ # define all routes urlpatterns = [ - path("downloadImage", views.downloadSingleImage, name="downloadSingleImage"), - path("bulkDownloadImages", views.bulkDownloadImages, name="bulkDownloadImages"), - path("uploadSingleImage", views.uploadSingleImage, name="uploadSingleImage"), - path("bulkUploadImages", views.bulkUploadImages, name="bulkUploadImage"), + path("uploadImage/", views.uploadImage, name="uploadImage"), + path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), path("listBlobContainers", views.listBlobContainers, name="listBlobContainers") ] diff --git a/imageController/views.py b/imageController/views.py index fa95d88..b1a70b6 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -1,83 +1,75 @@ +from io import BytesIO import os import json from time import time, ctime from imageController.models import InventoryImage from django.shortcuts import render, HttpResponse from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient +from azure.core.exceptions import ResourceExistsError from django.views.decorators.csrf import csrf_exempt +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.response import Response +from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, sanitizeName, removeStr +from CCPDController.authentication import JWTAuthentication +from CCPDController.permissions import IsQAPermission, IsAdminPermission from pymongo import MongoClient from dotenv import load_dotenv load_dotenv() # Azure Blob -# azure_blob_client = ContainerClient(os.getenv('SAS_KEY'), 'ccpd') -azure_blob_client = BlobServiceClient(os.getenv('SAS_KEY')) -container = azure_blob_client.get_container_client("product-image") -# container = client.get_container_client('product-image') +# blob client object from azure access keys +azure_blob_client = BlobServiceClient.from_connection_string(os.getenv('SAS_KEY')) +# container handle for product image +product_image_container = azure_blob_client.get_container_client("product-image") # MongoDB -mongo_client = MongoClient(os.getenv('DATABASE_URL')) -db = mongo_client['CCPD'] +db = get_db_client() collection = db['InventoryImage'] -# single image download -def downloadSingleImage(request): - if request.method == 'GET': - print(request) - return collection.find() - return HttpResponse("Please use GET method") - -# single image download -def bulkDownloadImages(request): - if request.method == 'GET': - print(request) - - - return HttpResponse("Please use GET method") - # download all images related to 1 sku +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission | IsAdminPermission]) def downloadAllImagesBySKU(request): - if request.method == 'GET': - print(request) + + print(request.data) + return Response('here is all the image for sku: ', status.HTTP_200_OK) # single image upload -# @csrf_exempt -def uploadSingleImage(request): - if request.method == 'PUT' and request.body: - - # decode body to python object - body = json.loads(request.body.decode('utf-8')) +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission | IsAdminPermission]) +def uploadImage(request, sku): + # request body is unreadable binary code + # sku will be in the path parameter + # request.FILES looks like this and is a multi-value dictionary + # { + # 'IMG_20231110_150642.jpg': [], + # 'IMG_20231110_150000.jpg': [] + # } + for name, value in request.FILES.items(): + imageName = sku + '/' + sku + '_' + name + try: + res = product_image_container.upload_blob(imageName, value.file) + print(res.url) + except ResourceExistsError: + return Response(imageName + 'Already Exist!', status.HTTP_409_CONFLICT) - # construct the blob object - blob = BlobClient.from_connection_string( - conn_str=os.getenv('BLOB_KEY'), - container_name="product-image", - blob_name=body["sku"] - ) + # construct database row object + # newInventoryImage = InventoryImage( + # time = str(ctime(time())), + # sku = body["sku"], + # owner = body["owner"], + # images = body["images"] + # ) - # upload blob to azure - blob.upload_blob(body["images"][0]) - - # construct database row object - newInventoryImage = InventoryImage( - time = str(ctime(time())), - sku = body["sku"], - owner = body["owner"], - images = body["images"] - ) - - # push data to MongoDB - # await collection.insert_one(newInventoryImage.__dict__) - - return HttpResponse("upload single image") - return HttpResponse("Please upload using PUT method with JSON body") + # push data to MongoDB + # await collection.insert_one(newInventoryImage.__dict__) -# bulk image upload -def bulkUploadImages(request): - print(request) - return HttpResponse("Multiple image upload happens here") + return Response("Upload Success", status.HTTP_200_OK) # list blob containers def listBlobContainers(request): if request.method == 'GET': - return HttpResponse(container.list_blob_names()) \ No newline at end of file + return HttpResponse(product_image_container.list_blob_names()) \ No newline at end of file diff --git a/inventoryController/views.py b/inventoryController/views.py index edbebea..4629060 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -11,12 +11,15 @@ from rest_framework import status from bson.objectid import ObjectId import json +import pymongo # pymongo db = get_db_client() collection = db['Inventory'] +user_collection = db['User'] # query param sku for inventory db row +# sku: string @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) @@ -30,19 +33,23 @@ def getInventoryBySku(request): if not sku: return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + # find the Q&A record res = collection.find_one({'sku': sku}, {'_id': 0}) + # get user info + user = user_collection.find_one({'_id': ObjectId(res['owner'])}, {'name': 1, 'userActive': 1, '_id': 0}) + # replace owner field in response + res['owner'] = user if not res: return Response(sku, status.HTTP_404_NOT_FOUND) return Response(res, status.HTTP_200_OK) # get all inventory by QA personal +# id: string @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) def getInventoryByOwnerId(request): - - print(request.body) try: body = decodeJSON(request.body) except: @@ -55,7 +62,7 @@ def getInventoryByOwnerId(request): # return all inventory from owner in array arr = [] - for inventory in collection.find({ 'owner': ownerId }): + for inventory in collection.find({ 'owner': ownerId }).sort('sku', pymongo.DESCENDING): inventory['_id'] = str(inventory['_id']) arr.append(inventory) @@ -66,14 +73,17 @@ def getInventoryByOwnerId(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) def createInventory(request): - body = decodeJSON(request.body) - sku = sanitizeSku(body['sku']) - comment = removeStr(body['comment']) + + try: + body = decodeJSON(request.body) + sku = sanitizeSku(body['sku']) + except: + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) # if sku exist return error inv = collection.find_one({'sku': body['sku']}) if inv: - return Response('SKU Already Existed') + return Response('SKU Already Existed', status.HTTP_409_CONFLICT) # construct new inventory newInventory = InventoryItem( @@ -90,7 +100,7 @@ def createInventory(request): # pymongo need dict or bson object res = collection.insert_one(newInventory.__dict__) - return Response(newInventory) + return Response('Inventory Created', status.HTTP_200_OK) # query param sku and body of new inventory info # sku: string From 86e6329c15f4e8d2a43926b6361b42d7818ea7c6 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 14 Nov 2023 18:44:04 -0500 Subject: [PATCH 011/107] fixed image and inventory service --- .dockerignore | 27 +++++++++++++++++++++++++++ Dockerfile | 0 docker-compose.yml | 1 + imageController/urls.py | 2 +- imageController/views.py | 28 +++++++++++++++++++++------- inventoryController/views.py | 10 ++++++---- requirements.txt | 0 7 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..06e4705 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +*.pyc +*.pyo +*.mo +*.db +*.css.map +*.egg-info +*.sql.gz +.cache +.project +.idea +.pydevproject +.idea/workspace.xml +.DS_Store +.git/ +.sass-cache +.vagrant/ +__pycache__ +dist +docs +env +logs +src/{{ project_name }}/settings/local.py +src/node_modules +web/media +web/static/CACHE +stats +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..85a60df --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1 @@ +version: '3.9' \ No newline at end of file diff --git a/imageController/urls.py b/imageController/urls.py index c71a3c9..ce95f25 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -3,7 +3,7 @@ # define all routes urlpatterns = [ - path("uploadImage/", views.uploadImage, name="uploadImage"), + path("uploadImage//", views.uploadImage, name="uploadImage"), path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), path("listBlobContainers", views.listBlobContainers, name="listBlobContainers") ] diff --git a/imageController/views.py b/imageController/views.py index b1a70b6..384d578 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -40,7 +40,7 @@ def downloadAllImagesBySKU(request): @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) -def uploadImage(request, sku): +def uploadImage(request, sku, owner): # request body is unreadable binary code # sku will be in the path parameter # request.FILES looks like this and is a multi-value dictionary @@ -48,14 +48,23 @@ def uploadImage(request, sku): # 'IMG_20231110_150642.jpg': [], # 'IMG_20231110_150000.jpg': [] # } + + # loop the files in the request for name, value in request.FILES.items(): + # add tags of owner and time info + inventory_tags = { + "sku": sku, + "time": str(ctime(time())), + "owner": owner + } + # images will be uploaded to the folder named after their sku imageName = sku + '/' + sku + '_' + name try: - res = product_image_container.upload_blob(imageName, value.file) - print(res.url) + res = product_image_container.upload_blob(imageName, value.file, tags=inventory_tags) except ResourceExistsError: return Response(imageName + 'Already Exist!', status.HTTP_409_CONFLICT) - + + # construct database row object # newInventoryImage = InventoryImage( # time = str(ctime(time())), @@ -67,9 +76,14 @@ def uploadImage(request, sku): # push data to MongoDB # await collection.insert_one(newInventoryImage.__dict__) - return Response("Upload Success", status.HTTP_200_OK) + return Response(res.url, status.HTTP_200_OK) # list blob containers +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) def listBlobContainers(request): - if request.method == 'GET': - return HttpResponse(product_image_container.list_blob_names()) \ No newline at end of file + + + return Response('Listing blob containers......', status.HTTP_200_OK) + \ No newline at end of file diff --git a/inventoryController/views.py b/inventoryController/views.py index 4629060..184009a 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -35,13 +35,16 @@ def getInventoryBySku(request): # find the Q&A record res = collection.find_one({'sku': sku}, {'_id': 0}) + if not res: + return Response('Record Not Found', status.HTTP_400_BAD_REQUEST) + # get user info user = user_collection.find_one({'_id': ObjectId(res['owner'])}, {'name': 1, 'userActive': 1, '_id': 0}) + if not user: + return Response('Owner Not Found', status.HTTP_404_NOT_FOUND) + # replace owner field in response res['owner'] = user - - if not res: - return Response(sku, status.HTTP_404_NOT_FOUND) return Response(res, status.HTTP_200_OK) # get all inventory by QA personal @@ -170,7 +173,6 @@ def updateInventoryBySku(request): # delete inventory by sku -# admin only @api_view(['DELETE']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 From 590cd9e9ab9e83bfa145c70ecafbaca4b092a8e1 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 16 Nov 2023 19:01:02 -0500 Subject: [PATCH 012/107] updated views --- CCPDController/utils.py | 13 +++-- imageController/views.py | 13 +++++ inventoryController/models.py | 6 +-- inventoryController/urls.py | 2 +- inventoryController/views.py | 91 +++++++++++++++++++++++++---------- 5 files changed, 90 insertions(+), 35 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index ef40c9e..c2dbcd8 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -8,10 +8,10 @@ from dotenv import load_dotenv load_dotenv() -# get pymongo client obj +# construct mongoDB client +client = MongoClient(os.getenv('DATABASE_URL'), maxPoolSize=2) +db_handle = client[os.getenv('DB_NAME')] def get_db_client(): - client = MongoClient(os.getenv('DATABASE_URL'), maxPoolSize=2) - db_handle = client[os.getenv('DB_NAME')] return db_handle # decode body from json to object @@ -27,7 +27,10 @@ def get_db_client(): max_sku = 7 min_sku = 4 -# check if body contains valid user information +# time format to convert from string to datetime +time_format = "%a %b %d %H:%M:%S %Y" + +# check if body contains valid user registration information def checkBody(body): if not inRange(body['name'], min_name, max_name): return False @@ -102,4 +105,4 @@ def sanitizePlatform(platform): # shelf location sanitize def sanitizeShelfLocation(shelfLocation): if not isinstance(shelfLocation, str): - return False + return False \ No newline at end of file diff --git a/imageController/views.py b/imageController/views.py index 384d578..79937de 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -27,6 +27,19 @@ db = get_db_client() collection = db['InventoryImage'] +# sample code from Microsoft for generating user delegation sas key +# def request_user_delegation_key(self, blob_service_client: BlobServiceClient) -> UserDelegationKey: +# # Get a user delegation key that's valid for 1 day +# delegation_key_start_time = datetime.datetime.now(datetime.timezone.utc) +# delegation_key_expiry_time = delegation_key_start_time + datetime.timedelta(days=1) + +# user_delegation_key = blob_service_client.get_user_delegation_key( +# key_start_time=delegation_key_start_time, +# key_expiry_time=delegation_key_expiry_time +# ) + +# return user_delegation_key + # download all images related to 1 sku @api_view(['POST']) @authentication_classes([JWTAuthentication]) diff --git a/inventoryController/models.py b/inventoryController/models.py index 70eae82..b0d3e09 100644 --- a/inventoryController/models.py +++ b/inventoryController/models.py @@ -24,9 +24,9 @@ class InventoryItem(models.Model): time: models.CharField(max_length=30) sku: models.IntegerField(max_length=10) itemCondition: models.CharField(max_length=14, choices=CONDITION_CHOISES) - comment: models.TextField() - link: models.TextField() - platform: models.CharField(max_length=16, choices=PLATFORM_CHOISES) + comment: models.TextField(max_length=100) + link: models.TextField(max_length=300) + platform: models.CharField(max_length=17, choices=PLATFORM_CHOISES) shelfLocation: models.CharField(max_length=4) amount: models.IntegerField(max_length=3) owner: models.CharField(max_length=32) diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 9f1220f..507e420 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -6,6 +6,6 @@ path("getInventoryBySku", views.getInventoryBySku, name="getInventoryBySku"), path("createInventory", views.createInventory, name="createInventory"), path("deleteInventoryBySku", views.deleteInventoryBySku, name="deleteInventoryBySku"), - path("updateInventoryBySku", views.updateInventoryBySku, name="updateInventoryBySku"), + path("updateInventoryBySku/", views.updateInventoryBySku, name="updateInventoryBySku"), path("getInventoryByOwnerId", views.getInventoryByOwnerId, name="getInventoryByOwnerId") ] diff --git a/inventoryController/views.py b/inventoryController/views.py index 184009a..f4a7c63 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -1,8 +1,8 @@ from time import time, ctime -from django.shortcuts import HttpResponse +from datetime import datetime, timedelta from django.views.decorators.csrf import csrf_exempt from inventoryController.models import InventoryItem -from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, sanitizeName, removeStr +from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, time_format from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -122,37 +122,52 @@ def createInventory(request): } } """ -@api_view(['POST']) +@api_view(['PUT']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) -def updateInventoryBySku(request): +def updateInventoryBySku(request, sku): try: # convert to object id body = decodeJSON(request.body) - sku = sanitizeSku(body['sku']) - newInv = body['newInventory'] + sku = sanitizeSku(sku) + if not sku: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + # check body + newInv = body['newInventoryInfo'] + newInventory = InventoryItem( + time = newInv['time'], + sku = newInv['sku'], + itemCondition = newInv['time'], + comment = newInv['comment'], + link = newInv['link'], + platform = newInv['platform'], + shelfLocation = newInv['shelfLocation'], + amount = newInv['amount'], + owner = newInv['owner'] + ) except: - return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) + return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) - # grab existing inventory + print(sku) + # check if inventory exists oldInv = collection.find_one({ 'sku': sku }) if not oldInv: return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) - try: - # construct new inventory - newInventory = InventoryItem( - time = str(ctime(time())), - sku = newInv['sku'] if newInv['sku'] is not None else oldInv['sku'], - itemCondition = newInv['itemCondition'] if newInv['itemCondition'] is not None else oldInv['itemCondition'], - comment = newInv['comment'] if newInv['comment'] is not None else oldInv['comment'], - link = newInv['link'] if newInv['link'] is not None else oldInv['link'], - platform = newInv['platform'] if newInv['platform'] is not None else oldInv['platform'], - shelfLocation = newInv['shelfLocation'] if newInv['shelfLocation'] is not None else oldInv['shelfLocation'], - amount = newInv['amount'] if newInv['amount'] is not None else oldInv['amount'] - ) - except: - return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) + # try: + # # construct new inventory + # newInventory = InventoryItem( + # time = str(ctime(time())), + # sku = newInv['sku'] if newInv['sku'] is not None else oldInv['sku'], + # itemCondition = newInv['itemCondition'] if newInv['itemCondition'] is not None else oldInv['itemCondition'], + # comment = newInv['comment'] if newInv['comment'] is not None else oldInv['comment'], + # link = newInv['link'] if newInv['link'] is not None else oldInv['link'], + # platform = newInv['platform'] if newInv['platform'] is not None else oldInv['platform'], + # shelfLocation = newInv['shelfLocation'] if newInv['shelfLocation'] is not None else oldInv['shelfLocation'], + # amount = newInv['amount'] if newInv['amount'] is not None else oldInv['amount'] + # ) + # except: + # return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) # update inventory res = collection.update_one( @@ -170,16 +185,40 @@ def updateInventoryBySku(request): } } ) + + return Response('Update Success', status.HTTP_200_OK) # delete inventory by sku +# QA personal can only delete record created within 24h +# sku: string @api_view(['DELETE']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) +@permission_classes([IsQAPermission]) def deleteInventoryBySku(request): try: body = decodeJSON(request.body) - sanitizeSku(body['sku']) + sku = sanitizeSku(body['sku']) except: - # delete inventory by sku - collection.find_one({ 'sku': body['sku'] }) + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) + if not sku: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + # pull time + res = collection.find_one({'sku': sku}, {'time': 1}) + if not res: + return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) + + + # calculate time left to delete, prevent delete if result delta seconds is negative + # convert form time string to time obj + timeCreated = datetime.strptime(res['time'], time_format) + one_day_later = timeCreated + timedelta(days=1) + today = datetime.now() + canDel = (one_day_later - today).total_seconds() > 0 + + # perform deletion or throw error + if canDel: + de = collection.delete_one({'sku': sku}) + return Response('Inventory Deleted', status.HTTP_200_OK) + return Response('Cannot Delete Inventory After 24H, Please Contact Admin', status.HTTP_403_FORBIDDEN) \ No newline at end of file From 8cf7cfc3e3c9c437ffe85246dd0784e221f8f7f4 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 17 Nov 2023 19:07:30 -0500 Subject: [PATCH 013/107] fixed minor issue need more research on axios receive http-only cookies --- CCPDController/permissions.py | 4 ++-- CCPDController/serializer.py | 6 +----- CCPDController/settings.py | 6 ++++-- inventoryController/views.py | 4 +--- userController/views.py | 7 ++++--- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py index 56a492f..0888d2c 100644 --- a/CCPDController/permissions.py +++ b/CCPDController/permissions.py @@ -7,8 +7,8 @@ collection = db['User'] # user object will be pass into here from authentication -# auth = ROLE -# user = { +# request.auth = ROLE +# request.user = { # '_id': ObjectId(xxx), # 'userActive': xxx, # 'role': xxx diff --git a/CCPDController/serializer.py b/CCPDController/serializer.py index d87c29e..a4c430a 100644 --- a/CCPDController/serializer.py +++ b/CCPDController/serializer.py @@ -5,8 +5,4 @@ class UserSerializer(serializers.ModelSerializer): class Meta(object): model = User fields = ["email", "password"] - -class StudentSerializer(serializers.ModelSerializer): - class Meta: - model = Student - fields = ["name", "roll", "city"] + \ No newline at end of file diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 02e1c09..3420a0d 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -15,7 +15,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [ + '192.168.2.62' +] # JWT Secret SECRET_KEY = os.getenv('JWT_SECRET') @@ -86,7 +88,7 @@ CSRF_TRUSTED_ORIGINS = [ "http://127.0.0.1:8100", "http://localhost:8100", - "http://192.168.2.62:8100" + "http://192.168.2.62:8100", ] # Django rest framework diff --git a/inventoryController/views.py b/inventoryController/views.py index f4a7c63..d980a27 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -146,9 +146,8 @@ def updateInventoryBySku(request, sku): owner = newInv['owner'] ) except: - return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) + return Response('Invalid Inventory Info', status.HTTP_406_NOT_ACCEPTABLE) - print(sku) # check if inventory exists oldInv = collection.find_one({ 'sku': sku }) if not oldInv: @@ -175,7 +174,6 @@ def updateInventoryBySku(request, sku): { '$set': { - 'sku': newInventory.sku, 'itemCondition': newInventory.itemCondition, 'comment': newInventory.comment, 'link': newInventory.link, diff --git a/userController/views.py b/userController/views.py index d679fa8..6715e59 100644 --- a/userController/views.py +++ b/userController/views.py @@ -20,7 +20,7 @@ collection = db['User'] # jwt token expiring time -expire_days = 1 +expire_days = 14 # will be called every time on open app @csrf_protect @@ -37,7 +37,7 @@ def checkToken(request): except jwt.DecodeError or UnicodeError: raise AuthenticationFailed('Invalid token') except jwt.ExpiredSignatureError: - raise AuthenticationFailed('Token has expired') + raise AuthenticationFailed('No Token') if token: user = collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1}) @@ -85,7 +85,7 @@ def login(request): except: return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) - # return the id and name + # return the id and name with token in http only cookie info = { 'id': str(ObjectId(user['_id'])), 'name': user['name'] @@ -95,6 +95,7 @@ def login(request): response = Response(info, status.HTTP_200_OK) response.set_cookie('token', token, httponly=True) response.set_cookie('csrftoken', get_token(request), httponly=True) + print(token) return response # get user information without password From 47c9f2bc3cd60c1160c9f11d4fa42f98936c6be5 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 18 Nov 2023 19:08:42 -0500 Subject: [PATCH 014/107] fix bug, added utility --- .vscode/settings.json | 3 ++- CCPDController/authentication.py | 4 ++-- CCPDController/settings.py | 14 +++++++++----- CCPDController/utils.py | 9 +++++++++ inventoryController/views.py | 24 ++++-------------------- userController/views.py | 5 +++-- 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ba77eac..67d6a18 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "[python]": { "editor.defaultFormatter": "ms-python.python" }, - "python.formatting.provider": "none" + "python.formatting.provider": "none", + "python.analysis.typeCheckingMode": "off" } \ No newline at end of file diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 9187133..48168c9 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -31,8 +31,8 @@ def authenticate_credentials(self, id): user = collection.find_one({'_id': uid}, {'userActive': 1, 'role': 1}) # check user activation status - if not user['userActive']: - raise AuthenticationFailed('User Inactive') + if not user or user['userActive'] == False: + raise AuthenticationFailed('User Not Found Or Inactive') # return type have to be tuple return (user, user['role']) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 3420a0d..c10e7d9 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -16,7 +16,8 @@ DEBUG = True ALLOWED_HOSTS = [ - '192.168.2.62' + '192.168.2.62', + '127.0.0.1' ] # JWT Secret @@ -71,24 +72,27 @@ WSGI_APPLICATION = 'CCPDController.wsgi.application' -# for http only cookies +# cookies setting SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = None # cors stuff CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ - "http://localhost:8100", "http://127.0.0.1:8100", - "http://192.168.2.62:8100" + "http://127.0.0.1:5173", + "http://192.168.2.62:8100", + "http://192.168.2.62:5173", ] # csrf stuff CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ "http://127.0.0.1:8100", - "http://localhost:8100", + "http://127.0.0.1:5173", "http://192.168.2.62:8100", + "http://192.168.2.62:5173", ] # Django rest framework diff --git a/CCPDController/utils.py b/CCPDController/utils.py index c2dbcd8..7a8d6d8 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -17,6 +17,15 @@ def get_db_client(): # decode body from json to object decodeJSON = lambda body : json.loads(body.decode('utf-8'))\ +# get client ip address +def get_client_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + # limit variables max_name = 40 min_name = 3 diff --git a/inventoryController/views.py b/inventoryController/views.py index d980a27..8b89ab5 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from django.views.decorators.csrf import csrf_exempt from inventoryController.models import InventoryItem -from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, time_format +from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, time_format, get_client_ip from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -10,7 +10,6 @@ from rest_framework.response import Response from rest_framework import status from bson.objectid import ObjectId -import json import pymongo # pymongo @@ -153,33 +152,18 @@ def updateInventoryBySku(request, sku): if not oldInv: return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) - # try: - # # construct new inventory - # newInventory = InventoryItem( - # time = str(ctime(time())), - # sku = newInv['sku'] if newInv['sku'] is not None else oldInv['sku'], - # itemCondition = newInv['itemCondition'] if newInv['itemCondition'] is not None else oldInv['itemCondition'], - # comment = newInv['comment'] if newInv['comment'] is not None else oldInv['comment'], - # link = newInv['link'] if newInv['link'] is not None else oldInv['link'], - # platform = newInv['platform'] if newInv['platform'] is not None else oldInv['platform'], - # shelfLocation = newInv['shelfLocation'] if newInv['shelfLocation'] is not None else oldInv['shelfLocation'], - # amount = newInv['amount'] if newInv['amount'] is not None else oldInv['amount'] - # ) - # except: - # return Response('Invalid Inventory Info', status.HTTP_400_BAD_REQUEST) - # update inventory res = collection.update_one( { 'sku': sku }, { '$set': { + 'amount': newInventory.amount, 'itemCondition': newInventory.itemCondition, - 'comment': newInventory.comment, - 'link': newInventory.link, 'platform': newInventory.platform, 'shelfLocation': newInventory.shelfLocation, - 'amount': newInventory.amount + 'comment': newInventory.comment, + 'link': newInventory.link, } } ) diff --git a/userController/views.py b/userController/views.py index 6715e59..ed494db 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,7 +5,7 @@ from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -41,6 +41,8 @@ def checkToken(request): if token: user = collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1}) + if user['userActive'] == False: + return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) if user: return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name']}, status.HTTP_200_OK) return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) @@ -95,7 +97,6 @@ def login(request): response = Response(info, status.HTTP_200_OK) response.set_cookie('token', token, httponly=True) response.set_cookie('csrftoken', get_token(request), httponly=True) - print(token) return response # get user information without password From 43873267716dfbe3ce4d63d88ebff024955c8d51 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Mon, 20 Nov 2023 19:01:07 -0500 Subject: [PATCH 015/107] fixed routes, added invitation code mech --- CCPDController/authentication.py | 10 +++--- CCPDController/utils.py | 6 +++- README.md | 22 ++++++++----- adminController/models.py | 15 ++++++++- adminController/views.py | 21 ++++++++++--- imageController/views.py | 20 +----------- inventoryController/views.py | 32 ++++++++++--------- requirements.txt | Bin 0 -> 4702 bytes userController/views.py | 52 +++++++++++++++++++++---------- 9 files changed, 109 insertions(+), 69 deletions(-) diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 48168c9..7fc04f6 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -30,10 +30,12 @@ def authenticate_credentials(self, id): # get only id status and role user = collection.find_one({'_id': uid}, {'userActive': 1, 'role': 1}) - # check user activation status - if not user or user['userActive'] == False: - raise AuthenticationFailed('User Not Found Or Inactive') - + # check user status + if not user: + raise AuthenticationFailed('User Not Found') + if user['userActive'] == False: + raise AuthenticationFailed('User Inactive') + # return type have to be tuple return (user, user['role']) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 7a8d6d8..fd709c7 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,3 +1,4 @@ +import datetime import os import json import jwt @@ -36,8 +37,11 @@ def get_client_ip(request): max_sku = 7 min_sku = 4 -# time format to convert from string to datetime +# convert from string to datetime +# example: Thu Oct 12 18:48:49 2023 time_format = "%a %b %d %H:%M:%S %Y" +def convertToTime(time_str): + return datetime.strptime(time_str, time_format) # check if body contains valid user registration information def checkBody(body): diff --git a/README.md b/README.md index cd915af..8e2be66 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,26 @@ ## Services - -``` -- Q&A Intentory Recording Form. -- Image Upload / Download for Azure. -- User Login Authentication. -- Admin Management Functions. -``` +- Q&A inventory form controller. +- Inventory image gallery controller. + - Upload and download image from Microsoft Azure. + - User gallery for manipulate images. +- User authentication controller. + - Routes for different roles +- Admin controller contains admin only functions. + - Full control over Q&A user group information. + - Full control over Q&A inventory information. + - Convert Q&A record to actual listable inventory. + - Manage retail and return inventory. ## Commands - ``` # Start local development server python manage.py runserver # Run deployment check python manage.py check --deploy + +# Generate requirement.txt +pip freeze > requirements.txt ``` diff --git a/adminController/models.py b/adminController/models.py index 71a8362..5bb2fb1 100644 --- a/adminController/models.py +++ b/adminController/models.py @@ -1,3 +1,16 @@ from django.db import models -# Create your models here. +class InvitationCode(models.Model): + code: models.CharField(max_length=100) + available: models.BooleanField(max_length=5) + exp: models.CharField(max_length=25) + + # constructor input all info + def __init__(self, code, available, exp) -> None: + self.code = code + self.available = available + self.exp = exp + + # return inventory sku + def __str__(self) -> str: + return str(self.code) \ No newline at end of file diff --git a/adminController/views.py b/adminController/views.py index a3915f9..604dc51 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -7,6 +7,7 @@ from django.middleware.csrf import get_token from bson.objectid import ObjectId from datetime import date, datetime, timedelta +from .models import InvitationCode from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -20,6 +21,7 @@ db = get_db_client() user_collection = db['User'] inventory_collection = db['Inventory'] +inv_collection = db['Invitations'] # admin jwt token expiring time admin_expire_days = 90 @@ -99,6 +101,11 @@ def adminLogin(request): response.set_cookie('csrftoken', get_token(request), httponly=True) return response +@csrf_protect +@api_view(['POST']) +@permission_classes([AllowAny]) +def registerAdmin(request): + return Response('Registration Success') # delete user by id # _id: string @@ -121,8 +128,7 @@ def deleteUserById(request): user_collection.delete_one({'_id': uid}) return Response('User Deleted', status.HTTP_200_OK) return Response('User Not Found', status.HTTP_404_NOT_FOUND) - - + # set any user status to be active or disabled # _id: string, # password: string @@ -153,15 +159,22 @@ def setUserActive(request): # admin generate invitation code for newly hired QA personal to join -@api_view(['GET']) +@api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def issueInvitationCode(request): - # generate a uuid for invitation code inviteCode = uuid.uuid4() print(inviteCode) + newCode = InvitationCode( + code = inviteCode, + available = True, + exp = 'like in an hour or something', + ) + + inv_collection.insert_one(newCode) + return Response('Invitation Code Created: '.join(inviteCode), status.HTTP_200_OK) # update anyones password by id diff --git a/imageController/views.py b/imageController/views.py index 79937de..f99519c 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -1,19 +1,14 @@ -from io import BytesIO import os -import json from time import time, ctime -from imageController.models import InventoryImage -from django.shortcuts import render, HttpResponse from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient from azure.core.exceptions import ResourceExistsError from django.views.decorators.csrf import csrf_exempt from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.response import Response -from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, sanitizeName, removeStr +from CCPDController.utils import decodeJSON, get_db_client from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission -from pymongo import MongoClient from dotenv import load_dotenv load_dotenv() @@ -27,19 +22,6 @@ db = get_db_client() collection = db['InventoryImage'] -# sample code from Microsoft for generating user delegation sas key -# def request_user_delegation_key(self, blob_service_client: BlobServiceClient) -> UserDelegationKey: -# # Get a user delegation key that's valid for 1 day -# delegation_key_start_time = datetime.datetime.now(datetime.timezone.utc) -# delegation_key_expiry_time = delegation_key_start_time + datetime.timedelta(days=1) - -# user_delegation_key = blob_service_client.get_user_delegation_key( -# key_start_time=delegation_key_start_time, -# key_expiry_time=delegation_key_expiry_time -# ) - -# return user_delegation_key - # download all images related to 1 sku @api_view(['POST']) @authentication_classes([JWTAuthentication]) diff --git a/inventoryController/views.py b/inventoryController/views.py index 8b89ab5..da199bc 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from django.views.decorators.csrf import csrf_exempt from inventoryController.models import InventoryItem -from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, time_format, get_client_ip +from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, convertToTime, get_client_ip from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -14,7 +14,7 @@ # pymongo db = get_db_client() -collection = db['Inventory'] +qa_collection = db['Inventory'] user_collection = db['User'] # query param sku for inventory db row @@ -33,7 +33,7 @@ def getInventoryBySku(request): return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) # find the Q&A record - res = collection.find_one({'sku': sku}, {'_id': 0}) + res = qa_collection.find_one({'sku': sku}, {'_id': 0}) if not res: return Response('Record Not Found', status.HTTP_400_BAD_REQUEST) @@ -64,7 +64,7 @@ def getInventoryByOwnerId(request): # return all inventory from owner in array arr = [] - for inventory in collection.find({ 'owner': ownerId }).sort('sku', pymongo.DESCENDING): + for inventory in qa_collection.find({ 'owner': ownerId }).sort('sku', pymongo.DESCENDING): inventory['_id'] = str(inventory['_id']) arr.append(inventory) @@ -75,7 +75,6 @@ def getInventoryByOwnerId(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) def createInventory(request): - try: body = decodeJSON(request.body) sku = sanitizeSku(body['sku']) @@ -83,7 +82,7 @@ def createInventory(request): return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) # if sku exist return error - inv = collection.find_one({'sku': body['sku']}) + inv = qa_collection.find_one({'sku': body['sku']}) if inv: return Response('SKU Already Existed', status.HTTP_409_CONFLICT) @@ -101,7 +100,7 @@ def createInventory(request): ) # pymongo need dict or bson object - res = collection.insert_one(newInventory.__dict__) + res = qa_collection.insert_one(newInventory.__dict__) return Response('Inventory Created', status.HTTP_200_OK) # query param sku and body of new inventory info @@ -128,15 +127,16 @@ def updateInventoryBySku(request, sku): try: # convert to object id body = decodeJSON(request.body) - sku = sanitizeSku(sku) + sku = sanitizeSku(int(sku)) if not sku: return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + # check body newInv = body['newInventoryInfo'] newInventory = InventoryItem( time = newInv['time'], sku = newInv['sku'], - itemCondition = newInv['time'], + itemCondition = newInv['itemCondition'], comment = newInv['comment'], link = newInv['link'], platform = newInv['platform'], @@ -148,12 +148,12 @@ def updateInventoryBySku(request, sku): return Response('Invalid Inventory Info', status.HTTP_406_NOT_ACCEPTABLE) # check if inventory exists - oldInv = collection.find_one({ 'sku': sku }) + oldInv = qa_collection.find_one({ 'sku': sku }) if not oldInv: return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) # update inventory - res = collection.update_one( + res = qa_collection.update_one( { 'sku': sku }, { '$set': @@ -168,9 +168,11 @@ def updateInventoryBySku(request, sku): } ) + # return update status + if not res: + return Response('Update Failed', status.HTTP_404_NOT_FOUND) return Response('Update Success', status.HTTP_200_OK) - # delete inventory by sku # QA personal can only delete record created within 24h # sku: string @@ -187,20 +189,20 @@ def deleteInventoryBySku(request): return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) # pull time - res = collection.find_one({'sku': sku}, {'time': 1}) + res = qa_collection.find_one({'sku': sku}, {'time': 1}) if not res: return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) # calculate time left to delete, prevent delete if result delta seconds is negative # convert form time string to time obj - timeCreated = datetime.strptime(res['time'], time_format) + timeCreated = convertToTime(res['time']) one_day_later = timeCreated + timedelta(days=1) today = datetime.now() canDel = (one_day_later - today).total_seconds() > 0 # perform deletion or throw error if canDel: - de = collection.delete_one({'sku': sku}) + de = qa_collection.delete_one({'sku': sku}) return Response('Inventory Deleted', status.HTTP_200_OK) return Response('Cannot Delete Inventory After 24H, Please Contact Admin', status.HTTP_403_FORBIDDEN) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ef5e7af3e1f46b1547e99dfe67710cb20e0e2b76 100644 GIT binary patch literal 4702 zcmai&-EUe)5XJYoQvVbpgFg~KPOwz}T)*9uokf+BEg)=?r{NQD?+kcqdt9F3_7FO{MEY9OxXwplyYXj9r9L{3j zSg}{)!~4j7SU&9X_+55m*)4Y1hCLR>T5XhJqt8WWLzOCr?7b4kt+<-%bD>OD`dkZH z)^!;6<_$FLXv$5jp9P*|eXN~xSxh>#X6iVYYP4_D#Yrt3$#boXBlOh*Ps2hCSCyz} ztCg>r)QP<)M zHmq_oP8(1d!Bhx|daJ*9X=P8YsR=V@?GSfb9>jv9!K^FA$~=Czl0a*==Yf$~v(c{X zSCl=9y+&%QekP9SF0*&=O@x-prY65%XX;?Xd`?ctb{INv&bRWwgiSBQ!bGx)KI`{o zXkZlU<~Z_=rR{*1T&+uIcBn7F4#6^+$GtPz6Ucx8D1ztv!2a&FowUBjmt4bM1S*6c*-@{lYyStD^V4 zVqMP-RQ9$db72#RE0b@by%g4JecADUAGP4D*zeM;Yd?>L81<=SL^)CS@!M(;tj^R; zhisZDSNnVK`zW((-7(4SlNhRV7@O=zThPItasw&qNtvb%ige$7kqr|D1Yh-iDx4V& zZ$82Bw3P+t#Wrd;npo`HWCn!;v z_w-!#G7X&=BNk|-Z{evrP!6r8CAXTqyCq*A;{4!Dr5nfku31fFs}A)>9ysHyE6`URujPeX3TFg7=6tU8!Ghc1N_KSfA`s>Yl=xk5Y+3QC zmRuSyYpq_?D{oXjJFW1weg_G69J0n!ttzNbgQJVUfDZERnN?2=O!9$H!eMq3`(1tX zj*ie%){RM=8DZ6*)STz5=pMMf7U!JBKhn*xcN556+)yf(JH@evj0H^Rk=0sWeMb6T zkqYvSV68RZWSNT0h*W#+VP7^L6lC3?|Lba-Wp0@)jjEcZ7CuhA=6W^Ui{Jy<=h2DgX z|MQ)v6weae7rEV6en8|KsUy60x#t;0tvD;0C1we`C!t)bextBHR!&)~T%-2BZ^ENh z>P#;5`(Nd^@}>L{yM4>f^B?c8V)~)O0mEeH*yw;6$)_htb}bRWk$hqCAU-{P^5n(F zT+s;|`D*kFuX8EY>SA6{4NkvQ8hhNSx#wKkIQ!|IM7@?zI*074PtcmTuaVD0e120@ zW`&<}h+8$l$;m|b&A;~dS!a{Ex2n!_#4&Sbn2cMo#=V4D<~fB$oefMc{NurQ5YSRV z&!1MDF0`I~mboxf%?~>9R83XaXB%*M9{Sdmr#|yFJ@w}Zy}+}6BRp-VHSoG`$;VY% zDq{L@j2Z_vnLA9@dV!Z literal 0 HcmV?d00001 diff --git a/userController/views.py b/userController/views.py index ed494db..762f255 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,7 +5,7 @@ from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, convertToTime from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -18,6 +18,7 @@ # pymongo db = get_db_client() collection = db['User'] +inv_collection = db['Invitations'] # jwt token expiring time expire_days = 14 @@ -30,6 +31,8 @@ def checkToken(request): # get token token = request.COOKIES.get('token') + if not token: + raise AuthenticationFailed('Token Not Found') # decode and return user id try: @@ -39,13 +42,15 @@ def checkToken(request): except jwt.ExpiredSignatureError: raise AuthenticationFailed('No Token') - if token: - user = collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1}) - if user['userActive'] == False: - return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) - if user: - return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name']}, status.HTTP_200_OK) - return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) + # check user status + user = collection.find_one({'_id': ObjectId(payload['id'])}, {'name': 1, 'role': 1, 'userActive': 1}) + if not user: + raise AuthenticationFailed('User Not Found') + if user['userActive'] == False: + return AuthenticationFailed('User Inactive') + + # return user information + return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name'] }, status.HTTP_200_OK) # login any user and issue jwt # _id: xxx @@ -141,18 +146,31 @@ def getUserById(request): @csrf_protect @api_view(['POST']) def registerUser(request): - # sanitization - body = checkBody(decodeJSON(request.body)) - if not body: - return Response('Invalid registration info!', status.HTTP_400_BAD_REQUEST) + try: + # sanitize + body = checkBody(decodeJSON(request.body)) + email = sanitizeEmail(body['email']) + pwd = sanitizePassword(body['password']) + # check if email exist in database + res = collection.find_one({ 'email': body['email'] }) + if res: + return Response('Email already existed!', status.HTTP_409_CONFLICT) + except: + return Response('Invalid Registration Info', status.HTTP_400_BAD_REQUEST) - # implement invitation code later + if email == False or pwd == False: + return Response('Invalid Email Or Password', status.HTTP_400_BAD_REQUEST) - # check if email exist in database - res = collection.find_one({ 'email': body['email'] }) - if res: - return Response('Email already existed!', status.HTTP_400_BAD_REQUEST) + # check if admin issues such code + code = inv_collection.find_one({'code': body['code']}) + if not code: + return Response('Invitation Code Not Found', status.HTTP_404_NOT_FOUND) + # check if token expired + expTime = convertToTime(body['exp']) + if ((expTime - datetime.now()).total_seconds() < 0): + return Response('Invitation Code Expired', status.HTTP_406_NOT_ACCEPTABLE) + # construct user newUser = User( name=body['name'], From 28377ff0df463f15b46cdaa5ca80220ba3f6baff Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 21 Nov 2023 19:01:03 -0500 Subject: [PATCH 016/107] Add a field to QA record obj, working on paged query --- CCPDController/utils.py | 9 ++++- adminController/views.py | 15 ++++++-- inventoryController/models.py | 14 ++++++- inventoryController/urls.py | 2 +- inventoryController/views.py | 69 ++++++++++++++++++++++++----------- userController/views.py | 3 +- 6 files changed, 82 insertions(+), 30 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index fd709c7..571d2ca 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime import os import json import jwt @@ -36,6 +36,8 @@ def get_client_ip(request): min_password = 8 max_sku = 7 min_sku = 4 +max_inv_code = 100 +min_inv_code = 10 # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 @@ -43,6 +45,11 @@ def get_client_ip(request): def convertToTime(time_str): return datetime.strptime(time_str, time_format) +def sanitizeInvCode(code): + if not isinstance(code, str): + return False + return code + # check if body contains valid user registration information def checkBody(body): if not inRange(body['name'], min_name, max_name): diff --git a/adminController/views.py b/adminController/views.py index 604dc51..35c01a8 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -157,23 +157,30 @@ def setUserActive(request): return Response('Updated User Activation Status', status.HTTP_200_OK) return Response('User Not Found') - # admin generate invitation code for newly hired QA personal to join @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def issueInvitationCode(request): + + # generate a uuid for invitation code inviteCode = uuid.uuid4() print(inviteCode) + expireTime = datetime.now() + + newCode = InvitationCode( code = inviteCode, available = True, - exp = 'like in an hour or something', + exp = '', ) - - inv_collection.insert_one(newCode) + + try: + inv_collection.insert_one(newCode) + except: + return Response('Database Error', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response('Invitation Code Created: '.join(inviteCode), status.HTTP_200_OK) diff --git a/inventoryController/models.py b/inventoryController/models.py index b0d3e09..96a0783 100644 --- a/inventoryController/models.py +++ b/inventoryController/models.py @@ -14,12 +14,20 @@ class InventoryItem(models.Model): ] PLATFORM_CHOISES = [ - ('Amazon', 'AMAZON'), + ('Amazon', 'Amazon'), ('eBay', 'eBay'), ('Official Website', 'Official Website'), ('Other', 'Other') ] + MARKETPLACE_CHOISES = [ + ('Hibid', 'Hibid'), + ('Retail', 'Retail'), + ('eBay', 'eBay'), + ('Wholesale', 'Wholesale'), + ('Other', 'Other') + ] + _id: models.AutoField(primary_key=True) time: models.CharField(max_length=30) sku: models.IntegerField(max_length=10) @@ -30,9 +38,10 @@ class InventoryItem(models.Model): shelfLocation: models.CharField(max_length=4) amount: models.IntegerField(max_length=3) owner: models.CharField(max_length=32) + marketplace: models.CharField(max_length=10, choices=MARKETPLACE_CHOISES) # constructor input all info - def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocation, amount, owner) -> None: + def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocation, amount, owner, marketplace) -> None: self.time = time self.sku = sku self.itemCondition=itemCondition @@ -42,6 +51,7 @@ def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocat self.shelfLocation = shelfLocation self.amount = amount self.owner = owner + self.marketplace = marketplace # return inventory sku def __str__(self) -> str: diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 507e420..358a695 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -7,5 +7,5 @@ path("createInventory", views.createInventory, name="createInventory"), path("deleteInventoryBySku", views.deleteInventoryBySku, name="deleteInventoryBySku"), path("updateInventoryBySku/", views.updateInventoryBySku, name="updateInventoryBySku"), - path("getInventoryByOwnerId", views.getInventoryByOwnerId, name="getInventoryByOwnerId") + path("getInventoryByOwnerId/", views.getInventoryByOwnerId, name="getInventoryByOwnerId") ] diff --git a/inventoryController/views.py b/inventoryController/views.py index da199bc..df5ad70 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -51,20 +51,42 @@ def getInventoryBySku(request): @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) -def getInventoryByOwnerId(request): +def getInventoryByOwnerId(request, page): try: body = decodeJSON(request.body) + ownerId = str(ObjectId(body['id'])) + + # TODO: make limit a path parameter + # get targeted page + limit = 10 + skip = page * limit except: - return Response('Invalid Body',status.HTTP_400_BAD_REQUEST) + return Response('Invalid Id', status.HTTP_400_BAD_REQUEST) + + # return all inventory from owner in array + arr = [] + skip = page * limit + for inventory in qa_collection.find({ 'owner': ownerId }).sort('sku', pymongo.DESCENDING).skip(skip).limit(limit): + inventory['_id'] = str(inventory['_id']) + arr.append(inventory) + + return Response(arr, status.HTTP_200_OK) + +# +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission]) +def getInventoryInfoByOwnerId(request): try: + body = decodeJSON(request.body) ownerId = str(ObjectId(body['id'])) except: return Response('Invalid Id', status.HTTP_400_BAD_REQUEST) - + # return all inventory from owner in array arr = [] - for inventory in qa_collection.find({ 'owner': ownerId }).sort('sku', pymongo.DESCENDING): + for inventory in qa_collection.find({ 'owner': ownerId }): inventory['_id'] = str(inventory['_id']) arr.append(inventory) @@ -81,26 +103,29 @@ def createInventory(request): except: return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) - # if sku exist return error + # if sku exist return conflict inv = qa_collection.find_one({'sku': body['sku']}) if inv: return Response('SKU Already Existed', status.HTTP_409_CONFLICT) - # construct new inventory - newInventory = InventoryItem( - time=str(ctime(time())), - sku=sku, - itemCondition=body['itemCondition'], - comment=body['comment'], - link=body['link'], - platform=body['platform'], - shelfLocation=body['shelfLocation'], - amount=body['amount'], - owner=body['owner'], - ) - - # pymongo need dict or bson object - res = qa_collection.insert_one(newInventory.__dict__) + try: + # construct new inventory + newInventory = InventoryItem( + time=str(ctime(time())), + sku=sku, + itemCondition=body['itemCondition'], + comment=body['comment'], + link=body['link'], + platform=body['platform'], + shelfLocation=body['shelfLocation'], + amount=body['amount'], + owner=body['owner'], + marketplace=body['marketplace'] + ) + # pymongo need dict or bson object + res = qa_collection.insert_one(newInventory.__dict__) + except: + return Response('Invalid Inventory Information', status.HTTP_400_BAD_REQUEST) return Response('Inventory Created', status.HTTP_200_OK) # query param sku and body of new inventory info @@ -142,7 +167,8 @@ def updateInventoryBySku(request, sku): platform = newInv['platform'], shelfLocation = newInv['shelfLocation'], amount = newInv['amount'], - owner = newInv['owner'] + owner = newInv['owner'], + marketplace = newInv['marketplace'] ) except: return Response('Invalid Inventory Info', status.HTTP_406_NOT_ACCEPTABLE) @@ -164,6 +190,7 @@ def updateInventoryBySku(request, sku): 'shelfLocation': newInventory.shelfLocation, 'comment': newInventory.comment, 'link': newInventory.link, + 'marketplace': newInventory.marketplace } } ) diff --git a/userController/views.py b/userController/views.py index 762f255..a1786a0 100644 --- a/userController/views.py +++ b/userController/views.py @@ -151,6 +151,7 @@ def registerUser(request): body = checkBody(decodeJSON(request.body)) email = sanitizeEmail(body['email']) pwd = sanitizePassword(body['password']) + invCode = body['code'] # check if email exist in database res = collection.find_one({ 'email': body['email'] }) if res: @@ -162,7 +163,7 @@ def registerUser(request): return Response('Invalid Email Or Password', status.HTTP_400_BAD_REQUEST) # check if admin issues such code - code = inv_collection.find_one({'code': body['code']}) + code = inv_collection.find_one({'code': invCode}) if not code: return Response('Invitation Code Not Found', status.HTTP_404_NOT_FOUND) From e1186497a44ce6ab1142e7c1e86b75333d229374 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 22 Nov 2023 18:58:41 -0500 Subject: [PATCH 017/107] added docker stuff --- CCPDController/settings.py | 2 +- Dockerfile | 21 +++++++++++++++++++++ docker-compose.yml | 23 ++++++++++++++++++++++- requirements.txt | Bin 4702 -> 1502 bytes 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index c10e7d9..c92ee6a 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -13,7 +13,7 @@ SECRET_KEY = 'django-insecure-x9@&zufge$doq71yfj!wfl*9ke=5&^+e-yjn*p+-97wz1)w)y1' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [ '192.168.2.62', diff --git a/Dockerfile b/Dockerfile index e69de29..b8ea046 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,21 @@ +# pull the python base image +FROM python:3.12.0-bookworm + +# set work directory +WORKDIR /usr/src/app + +# set env variable +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies from requirements txt file +RUN pip install --upgrade pip +COPY ./requirements.txt /usr/src/app +RUN pip install -r requirements.txt + +# copy project +COPY . /usr/src/app + +EXPOSE 8000 + +CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 85a60df..7f6a397 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1 +1,22 @@ -version: '3.9' \ No newline at end of file +# version: '3' + +# services: +# # Django service +# django: +# image: python:3.8 +# container_name: django-container +# volumes: +# - /usr/src/app +# working_dir: /app +# command: bash -c "pip install -r requirements.txt && python manage.py runserver 0.0.0.0:8000" +# ports: +# - "8000:8000" +# depends_on: +# - mongo + +# # MongoDB service +# mongo: +# image: mongo:latest +# container_name: mongo-container +# ports: +# - "27017:27017" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ef5e7af3e1f46b1547e99dfe67710cb20e0e2b76..e834bd456218484aa8f58b9ad5f25bb7f2696e07 100644 GIT binary patch delta 120 zcmcboa*vzo|G$X~lqPF1CQKG%+A^7qS!?nvW(P*|$+wv=3T85-Fyt{LGS~v4F@qk1 z$>d^I)yXn!YLmM-6ehEADNWwSr7-y)m)m3s9+k-nJbH{~llyr*Cv)&BO-=w}4L+60 YfB0M`H}ES=o*_^-*+I~4vWk!u0N#%zCIA2c literal 4702 zcmai&-EUe)5XJYoQvVbpgFg~KPOwz}T)*9uokf+BEg)=?r{NQD?+kcqdt9F3_7FO{MEY9OxXwplyYXj9r9L{3j zSg}{)!~4j7SU&9X_+55m*)4Y1hCLR>T5XhJqt8WWLzOCr?7b4kt+<-%bD>OD`dkZH z)^!;6<_$FLXv$5jp9P*|eXN~xSxh>#X6iVYYP4_D#Yrt3$#boXBlOh*Ps2hCSCyz} ztCg>r)QP<)M zHmq_oP8(1d!Bhx|daJ*9X=P8YsR=V@?GSfb9>jv9!K^FA$~=Czl0a*==Yf$~v(c{X zSCl=9y+&%QekP9SF0*&=O@x-prY65%XX;?Xd`?ctb{INv&bRWwgiSBQ!bGx)KI`{o zXkZlU<~Z_=rR{*1T&+uIcBn7F4#6^+$GtPz6Ucx8D1ztv!2a&FowUBjmt4bM1S*6c*-@{lYyStD^V4 zVqMP-RQ9$db72#RE0b@by%g4JecADUAGP4D*zeM;Yd?>L81<=SL^)CS@!M(;tj^R; zhisZDSNnVK`zW((-7(4SlNhRV7@O=zThPItasw&qNtvb%ige$7kqr|D1Yh-iDx4V& zZ$82Bw3P+t#Wrd;npo`HWCn!;v z_w-!#G7X&=BNk|-Z{evrP!6r8CAXTqyCq*A;{4!Dr5nfku31fFs}A)>9ysHyE6`URujPeX3TFg7=6tU8!Ghc1N_KSfA`s>Yl=xk5Y+3QC zmRuSyYpq_?D{oXjJFW1weg_G69J0n!ttzNbgQJVUfDZERnN?2=O!9$H!eMq3`(1tX zj*ie%){RM=8DZ6*)STz5=pMMf7U!JBKhn*xcN556+)yf(JH@evj0H^Rk=0sWeMb6T zkqYvSV68RZWSNT0h*W#+VP7^L6lC3?|Lba-Wp0@)jjEcZ7CuhA=6W^Ui{Jy<=h2DgX z|MQ)v6weae7rEV6en8|KsUy60x#t;0tvD;0C1we`C!t)bextBHR!&)~T%-2BZ^ENh z>P#;5`(Nd^@}>L{yM4>f^B?c8V)~)O0mEeH*yw;6$)_htb}bRWk$hqCAU-{P^5n(F zT+s;|`D*kFuX8EY>SA6{4NkvQ8hhNSx#wKkIQ!|IM7@?zI*074PtcmTuaVD0e120@ zW`&<}h+8$l$;m|b&A;~dS!a{Ex2n!_#4&Sbn2cMo#=V4D<~fB$oefMc{NurQ5YSRV z&!1MDF0`I~mboxf%?~>9R83XaXB%*M9{Sdmr#|yFJ@w}Zy}+}6BRp-VHSoG`$;VY% zDq{L@j2Z_vnLA9@dV!Z From bbd9e710fe6d6190244c68fd21847988f7e3d8d5 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 23 Nov 2023 18:59:46 -0500 Subject: [PATCH 018/107] prepare for container deployment --- CCPDController/authentication.py | 2 +- CCPDController/settings.py | 32 +++++++++++++++++++++++++------- CCPDController/utils.py | 7 ++++--- adminController/views.py | 4 ++-- docker-compose.yml | 22 ---------------------- inventoryController/urls.py | 3 ++- inventoryController/views.py | 20 +++++++++++++------- userController/views.py | 13 ++++++------- 8 files changed, 53 insertions(+), 50 deletions(-) delete mode 100644 docker-compose.yml diff --git a/CCPDController/authentication.py b/CCPDController/authentication.py index 7fc04f6..ee47ce6 100644 --- a/CCPDController/authentication.py +++ b/CCPDController/authentication.py @@ -48,7 +48,7 @@ def authenticate(self, request): raise AuthenticationFailed('No token provided') # decode jwt and retrive user id - payload = jwt.decode(raw_token, settings.SECRET_KEY, algorithms='HS256') + payload = jwt.decode(raw_token, settings.JWT_SECRET_KEY, algorithms='HS256') except jwt.DecodeError or UnicodeError: raise AuthenticationFailed('Invalid token') diff --git a/CCPDController/settings.py b/CCPDController/settings.py index c92ee6a..980bdc3 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -10,18 +10,22 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-x9@&zufge$doq71yfj!wfl*9ke=5&^+e-yjn*p+-97wz1)w)y1' +SECRET_KEY = 'whos-your-daddy-django-insecure-x9@&zufge$doq71yfj!wfl*9ke=5&^+e-yjn*p+-97wz1)w)y1' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True -ALLOWED_HOSTS = [ - '192.168.2.62', - '127.0.0.1' -] +# ALLOWED_HOSTS = [ +# '192.168.2.62', +# '127.0.0.1', +# '142.126.96.24', +# 'localhost' +# ] + +ALLOWED_HOSTS = ['*'] # JWT Secret -SECRET_KEY = os.getenv('JWT_SECRET') +JWT_SECRET_KEY = os.getenv('JWT_SECRET') # Application definition INSTALLED_APPS = [ @@ -72,6 +76,12 @@ WSGI_APPLICATION = 'CCPDController.wsgi.application' +# https +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +# SECURE_SSL_REDIRECT = True +SECURE_HSTS_SECONDS = 31536000 + # cookies setting SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = None @@ -80,6 +90,10 @@ CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ + # "http://localhost", + # "https://localhost", + "http://142.126.96.24", + "https://142.126.96.24", "http://127.0.0.1:8100", "http://127.0.0.1:5173", "http://192.168.2.62:8100", @@ -89,6 +103,10 @@ # csrf stuff CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ + # "http://localhost", + # "https://localhost", + "http://142.126.96.24", + "https://142.126.96.24", "http://127.0.0.1:8100", "http://127.0.0.1:5173", "http://192.168.2.62:8100", diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 571d2ca..e74e9ad 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,7 +1,8 @@ from datetime import datetime import os import json -import jwt +import certifi +import ssl from django.conf import settings from rest_framework.response import Response from rest_framework import exceptions @@ -10,13 +11,13 @@ load_dotenv() # construct mongoDB client -client = MongoClient(os.getenv('DATABASE_URL'), maxPoolSize=2) +client = MongoClient(os.getenv('DATABASE_URL'), maxPoolSize=1, tlsCAFile=certifi.where()) db_handle = client[os.getenv('DB_NAME')] def get_db_client(): return db_handle # decode body from json to object -decodeJSON = lambda body : json.loads(body.decode('utf-8'))\ +decodeJSON = lambda body : json.loads(body.decode('utf-8')) # get client ip address def get_client_ip(request): diff --git a/adminController/views.py b/adminController/views.py index 35c01a8..b00f2e6 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -36,7 +36,7 @@ def checkAdminToken(request): # decode and return user id try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms='HS256') + payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms='HS256') except jwt.DecodeError or UnicodeError: raise AuthenticationFailed('Invalid token') except jwt.ExpiredSignatureError: @@ -85,7 +85,7 @@ def adminLogin(request): } # construct tokent and return it - token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") except: return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 7f6a397..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -# version: '3' - -# services: -# # Django service -# django: -# image: python:3.8 -# container_name: django-container -# volumes: -# - /usr/src/app -# working_dir: /app -# command: bash -c "pip install -r requirements.txt && python manage.py runserver 0.0.0.0:8000" -# ports: -# - "8000:8000" -# depends_on: -# - mongo - -# # MongoDB service -# mongo: -# image: mongo:latest -# container_name: mongo-container -# ports: -# - "27017:27017" \ No newline at end of file diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 358a695..c800b02 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -7,5 +7,6 @@ path("createInventory", views.createInventory, name="createInventory"), path("deleteInventoryBySku", views.deleteInventoryBySku, name="deleteInventoryBySku"), path("updateInventoryBySku/", views.updateInventoryBySku, name="updateInventoryBySku"), - path("getInventoryByOwnerId/", views.getInventoryByOwnerId, name="getInventoryByOwnerId") + path("getInventoryByOwnerId/", views.getInventoryByOwnerId, name="getInventoryByOwnerId"), + path("getInventoryInfoByOwnerId", views.getInventoryInfoByOwnerId, name="getInventoryInfoByOwnerId") ] diff --git a/inventoryController/views.py b/inventoryController/views.py index df5ad70..cd22b55 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from rest_framework import status from bson.objectid import ObjectId +from collections import Counter import pymongo # pymongo @@ -73,10 +74,10 @@ def getInventoryByOwnerId(request, page): return Response(arr, status.HTTP_200_OK) -# +# id: string @api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission]) +@permission_classes([IsQAPermission | IsAdminPermission]) def getInventoryInfoByOwnerId(request): try: body = decodeJSON(request.body) @@ -84,13 +85,18 @@ def getInventoryInfoByOwnerId(request): except: return Response('Invalid Id', status.HTTP_400_BAD_REQUEST) - # return all inventory from owner in array + # array of all inventory arr = [] - for inventory in qa_collection.find({ 'owner': ownerId }): - inventory['_id'] = str(inventory['_id']) - arr.append(inventory) + cursor = qa_collection.find({ 'owner': ownerId }, { 'itemCondition': 1 }) + for inventory in cursor: + # inventory['_id'] = str(inventory['_id']) + arr.append(inventory['itemCondition']) - return Response(arr, status.HTTP_200_OK) + itemCount = Counter() + for condition in arr: + itemCount[condition] += 1 + + return Response(dict(itemCount), status.HTTP_200_OK) # create single inventory Q&A record @api_view(['PUT']) diff --git a/userController/views.py b/userController/views.py index a1786a0..06897bd 100644 --- a/userController/views.py +++ b/userController/views.py @@ -36,7 +36,7 @@ def checkToken(request): # decode and return user id try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms='HS256') + payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms='HS256') except jwt.DecodeError or UnicodeError: raise AuthenticationFailed('Invalid token') except jwt.ExpiredSignatureError: @@ -54,7 +54,6 @@ def checkToken(request): # login any user and issue jwt # _id: xxx -@csrf_protect @api_view(['POST']) @permission_classes([AllowAny]) def login(request): @@ -88,7 +87,7 @@ def login(request): } # construct tokent and return it - token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") except: return Response('Failed to Generate Token', status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -167,10 +166,10 @@ def registerUser(request): if not code: return Response('Invitation Code Not Found', status.HTTP_404_NOT_FOUND) - # check if token expired - expTime = convertToTime(body['exp']) - if ((expTime - datetime.now()).total_seconds() < 0): - return Response('Invitation Code Expired', status.HTTP_406_NOT_ACCEPTABLE) + # # check if token expired + # expTime = convertToTime(body['exp']) + # if ((expTime - datetime.now()).total_seconds() < 0): + # return Response('Invitation Code Expired', status.HTTP_406_NOT_ACCEPTABLE) # construct user newUser = User( From 6f838dd490c9f89c625bc0c7c240bb46249258a8 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 24 Nov 2023 18:58:19 -0500 Subject: [PATCH 019/107] prepare for deployment --- CCPDController/settings.py | 30 +++++++++++++++++------------- CCPDController/utils.py | 8 +++++--- userController/views.py | 18 ++++++++++-------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 980bdc3..6622af3 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -79,7 +79,7 @@ # https CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True -# SECURE_SSL_REDIRECT = True +SECURE_SSL_REDIRECT = False # http -> https SECURE_HSTS_SECONDS = 31536000 # cookies setting @@ -90,27 +90,31 @@ CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ + 'http://172.18.208.1', + 'https://172.18.208.1', # "http://localhost", # "https://localhost", - "http://142.126.96.24", - "https://142.126.96.24", - "http://127.0.0.1:8100", - "http://127.0.0.1:5173", - "http://192.168.2.62:8100", - "http://192.168.2.62:5173", + # "http://142.126.96.24", + # "https://142.126.96.24", + # "http://127.0.0.1:8100", + # "http://127.0.0.1:5173", + # "http://192.168.2.62:8100", + # "http://192.168.2.62:5173", ] # csrf stuff CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ + 'http://172.18.208.1', + 'https://172.18.208.1', # "http://localhost", # "https://localhost", - "http://142.126.96.24", - "https://142.126.96.24", - "http://127.0.0.1:8100", - "http://127.0.0.1:5173", - "http://192.168.2.62:8100", - "http://192.168.2.62:5173", + # "http://142.126.96.24", + # "https://142.126.96.24", + # "http://127.0.0.1:8100", + # "http://127.0.0.1:5173", + # "http://192.168.2.62:8100", + # "http://192.168.2.62:5173", ] # Django rest framework diff --git a/CCPDController/utils.py b/CCPDController/utils.py index e74e9ad..ede2487 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,8 +1,6 @@ from datetime import datetime import os import json -import certifi -import ssl from django.conf import settings from rest_framework.response import Response from rest_framework import exceptions @@ -11,7 +9,11 @@ load_dotenv() # construct mongoDB client -client = MongoClient(os.getenv('DATABASE_URL'), maxPoolSize=1, tlsCAFile=certifi.where()) +# ssl hand shake error because ip not whitelisted +client = MongoClient( + os.getenv('DATABASE_URL'), + maxPoolSize=1 +) db_handle = client[os.getenv('DB_NAME')] def get_db_client(): return db_handle diff --git a/userController/views.py b/userController/views.py index 06897bd..f8b20b9 100644 --- a/userController/views.py +++ b/userController/views.py @@ -56,15 +56,17 @@ def checkToken(request): # _id: xxx @api_view(['POST']) @permission_classes([AllowAny]) -def login(request): - body = decodeJSON(request.body) +def login(request): + try: + body = decodeJSON(request.body) + # sanitize + email = sanitizeEmail(body['email']) + password = sanitizePassword(body['password']) + if email == False or password == False: + return Response('Invalid Email Or Password', status.HTTP_400_BAD_REQUEST) + except: + return Response('Invalid Login Info', status.HTTP_400_BAD_REQUEST) - # sanitize - email = sanitizeEmail(body['email']) - password = sanitizePassword(body['password']) - if email == False or password == False: - return Response('Invalid Login Information', status.HTTP_400_BAD_REQUEST) - # check if user exist # only retrive user status and role user = collection.find_one({ From bceaee1ebe7ed6716802a75ddc8ff173deb48257 Mon Sep 17 00:00:00 2001 From: CccrizzZ <30523986+CccrizzZ@users.noreply.github.com> Date: Sat, 25 Nov 2023 21:14:58 -0500 Subject: [PATCH 020/107] Create docker-image.yml --- .github/workflows/docker-image.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..a090705 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag cccrizzz/ccpd-django-service:latest From 4ee84f4544a735d865e7f33eec0db7c368b94b13 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 28 Nov 2023 19:01:49 -0500 Subject: [PATCH 021/107] solved cookie problem, working on docker cert problem option for set cookies should set secured to boolean true and samesite to string "None" --- CCPDController/settings.py | 28 ++++++++++++++++------------ Dockerfile | 8 +++++++- requirements.txt | Bin 1502 -> 1836 bytes userController/views.py | 13 ++++++++----- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 6622af3..9691f5f 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -29,6 +29,8 @@ # Application definition INSTALLED_APPS = [ + 'werkzeug_debugger_runserver', + 'django_extensions', 'rest_framework', 'corsheaders', 'django_user_agents', @@ -76,22 +78,24 @@ WSGI_APPLICATION = 'CCPDController.wsgi.application' -# https +# https settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True SECURE_SSL_REDIRECT = False # http -> https -SECURE_HSTS_SECONDS = 31536000 +# SECURE_HSTS_SECONDS = 31536000 -# cookies setting +# cookies settings SESSION_COOKIE_HTTPONLY = True -SESSION_COOKIE_SAMESITE = None +SESSION_COOKIE_SAMESITE = 'None' +CSRF_COOKIE_SAMESITE = 'None' -# cors stuff -CORS_ALLOW_ALL_ORIGINS = True + +# cors settings CORS_ALLOW_CREDENTIALS = True -CORS_ALLOWED_ORIGINS = [ - 'http://172.18.208.1', - 'https://172.18.208.1', +CORS_ALLOW_ALL_ORIGINS = True +# CORS_ALLOWED_ORIGINS = [ + # 'http://172.18.208.1', + # 'https://172.18.208.1', # "http://localhost", # "https://localhost", # "http://142.126.96.24", @@ -100,9 +104,9 @@ # "http://127.0.0.1:5173", # "http://192.168.2.62:8100", # "http://192.168.2.62:5173", -] +# ] -# csrf stuff +# csrf settings CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ 'http://172.18.208.1', @@ -127,7 +131,7 @@ # Name of cache backend to cache user agents. If it not specified default # cache alias will be used. Set to `None` to disable caching. -USER_AGENTS_CACHE = None +# USER_AGENTS_CACHE = None # DATABASES = { # 'default': { diff --git a/Dockerfile b/Dockerfile index b8ea046..245119c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,15 @@ RUN pip install --upgrade pip COPY ./requirements.txt /usr/src/app RUN pip install -r requirements.txt +# Copy the SSL certificate files into the image +COPY certs/cert.pem /usr/local/share/ca-certificates/cert.pem +COPY certs/key.pem /usr/local/share/ca-certificates/key.pem + +RUN update-ca-certificates + # copy project COPY . /usr/src/app EXPOSE 8000 -CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ] \ No newline at end of file +CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000", "--cert-file", "certs/cert.pem", "--key-file", "certs/key.pem" ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e834bd456218484aa8f58b9ad5f25bb7f2696e07..1c4c9f2a3d7f9335fa16fb2249622cf9f25c1f06 100644 GIT binary patch delta 297 zcmcb|y@qc?3?rlYa=JkIQieQ+Vlb}^OxiNoG8lkN0b;|+f=tPxDGd1x$v|AdPy&>L$eK;g zW@@$fWk>|uPzsa_2C~zDCV*6cTww?{gO`B|XnrX}E>J}!SPmp^#9#&@C%A220|aGZjjx^n@=;hGBO%W4rC3K4F@|7;T4c2AVnaD8cv?e>L^tSc1k+f0I(=f J#*zVKJphFXIJ^J= delta 44 zcmV+{0Mq}h4&Do}Tmb Date: Thu, 30 Nov 2023 10:52:33 -0500 Subject: [PATCH 022/107] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_django-server.yml | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/main_django-server.yml diff --git a/.github/workflows/main_django-server.yml b/.github/workflows/main_django-server.yml new file mode 100644 index 0000000..e230086 --- /dev/null +++ b/.github/workflows/main_django-server.yml @@ -0,0 +1,69 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions +# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions + +name: Build and deploy Python app to Azure Web App - django-server + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python version + uses: actions/setup-python@v1 + with: + python-version: '3.12' + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + # Optional: Add step to run tests here (PyTest, Django test suites, etc.) + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment jobs + uses: actions/upload-artifact@v3 + with: + name: python-app + path: | + release.zip + !venv/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: python-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + + - name: 'Deploy to Azure Web App' + uses: azure/webapps-deploy@v2 + id: deploy-to-webapp + with: + app-name: 'django-server' + slot-name: 'Production' + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_712BCD92B3AC4A4D850C8E83C0DB4F40 }} \ No newline at end of file From 6abbf761b7c7a7e0169289c379aa595e54a2632e Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 30 Nov 2023 18:45:25 -0500 Subject: [PATCH 023/107] deployment --- .github/workflows/main_django-server.yml | 69 ------------------------ .gitignore | 2 + CCPDController/settings.py | 11 ++-- Dockerfile | 14 ++--- certs/ccpowerdeals.ca.crt | 29 ++++++++++ docker-compose.yml | 7 +++ userController/urls.py | 1 + userController/views.py | 8 ++- 8 files changed, 60 insertions(+), 81 deletions(-) delete mode 100644 .github/workflows/main_django-server.yml create mode 100644 certs/ccpowerdeals.ca.crt create mode 100644 docker-compose.yml diff --git a/.github/workflows/main_django-server.yml b/.github/workflows/main_django-server.yml deleted file mode 100644 index e230086..0000000 --- a/.github/workflows/main_django-server.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions -# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions - -name: Build and deploy Python app to Azure Web App - django-server - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python version - uses: actions/setup-python@v1 - with: - python-version: '3.12' - - - name: Create and start virtual environment - run: | - python -m venv venv - source venv/bin/activate - - - name: Install dependencies - run: pip install -r requirements.txt - - # Optional: Add step to run tests here (PyTest, Django test suites, etc.) - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment jobs - uses: actions/upload-artifact@v3 - with: - name: python-app - path: | - release.zip - !venv/ - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: python-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - - name: 'Deploy to Azure Web App' - uses: azure/webapps-deploy@v2 - id: deploy-to-webapp - with: - app-name: 'django-server' - slot-name: 'Production' - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_712BCD92B3AC4A4D850C8E83C0DB4F40 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b456cf5..f5fd316 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ wheels/ *.egg-info/ .installed.cfg *.egg +staticfiles/ +static/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 9691f5f..b2074b3 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -4,7 +4,8 @@ load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +# BASE_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ @@ -13,7 +14,7 @@ SECRET_KEY = 'whos-your-daddy-django-insecure-x9@&zufge$doq71yfj!wfl*9ke=5&^+e-yjn*p+-97wz1)w)y1' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv("DEBUG", "False") == "True" # ALLOWED_HOSTS = [ # '192.168.2.62', @@ -81,7 +82,8 @@ # https settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True -SECURE_SSL_REDIRECT = False # http -> https +SECURE_SSL_REDIRECT = True # http -> https +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # SECURE_HSTS_SECONDS = 31536000 # cookies settings @@ -89,7 +91,6 @@ SESSION_COOKIE_SAMESITE = 'None' CSRF_COOKIE_SAMESITE = 'None' - # cors settings CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = True @@ -174,7 +175,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ +# for manage.py collectstatic command STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field diff --git a/Dockerfile b/Dockerfile index 245119c..99a5685 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,15 +13,15 @@ RUN pip install --upgrade pip COPY ./requirements.txt /usr/src/app RUN pip install -r requirements.txt -# Copy the SSL certificate files into the image -COPY certs/cert.pem /usr/local/share/ca-certificates/cert.pem -COPY certs/key.pem /usr/local/share/ca-certificates/key.pem - -RUN update-ca-certificates - # copy project COPY . /usr/src/app +# Copy the SSL certificate files into the image +# COPY certs/certificate.pem /usr/local/share/ca-certificates/certificate.pem +# COPY certs/key.pem /usr/local/share/ca-certificates/key.pem +# COPY certs/ccpowerdeals.ca.crt /usr/local/share/ca-certificates/ +# RUN update-ca-certificates + EXPOSE 8000 -CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000", "--cert-file", "certs/cert.pem", "--key-file", "certs/key.pem" ] \ No newline at end of file +CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000" ] \ No newline at end of file diff --git a/certs/ccpowerdeals.ca.crt b/certs/ccpowerdeals.ca.crt new file mode 100644 index 0000000..7c0b13f --- /dev/null +++ b/certs/ccpowerdeals.ca.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE/zCCA+egAwIBAgISAwnfBd881ERWeQ3+NZWvNN3bMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMzEwMTMxMjAyMjRaFw0yNDAxMTExMjAyMjNaMBwxGjAYBgNVBAMM +ESouY2Nwb3dlcmRlYWxzLmNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArYKiu7HBNbLXDA28DuLkY2VGVwYaJ1JPm7ctD8PzKWlZf6g7ezBNK67I7U5+ +qMEhOFyqM3tuEMXGkgwnRh7mAVrDivPbk2dAuFQW6iGkHVE8EEj+XS9F2G7Q31Yl +W0PIuzZkk6Fofb+BECjjfrA0lhkAwjRK0U4+Opo5AyevVLcr+yyWnwWGOSM+n1NI +eZQmS+hvQCSI9DolVbGea/RWg5HsvpaxpHE4Q4MT+rh84ObildnUxLrXCky+c3ZA +9/QN3ex9zAR6OYvat86MF6DS7w1MiupIKbRi6iS0Fa5WbD8uFT/Jt1EBD1Y5qhCe +C7gkEkhRU5ZQ4l5WoInZAwmWNQIDAQABo4ICIzCCAh8wDgYDVR0PAQH/BAQDAgWg +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0G +A1UdDgQWBBSg0n93nqhbMGYil9skIFgI+RLMnDAfBgNVHSMEGDAWgBQULrMXt1hW +y65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6 +Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iu +b3JnLzAtBgNVHREEJjAkghEqLmNjcG93ZXJkZWFscy5jYYIPY2Nwb3dlcmRlYWxz +LmNhMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDv +AHUAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcAAAGLKSGDwwAABAMA +RjBEAiAIdAavNFbd0hLvHYmQSoFFiygryXJyA7pC44XPN1ytOgIgPR90NWP8Z/fz +7Cebck37xiCxBTX6NdGYvm2i4LNHnnwAdgDuzdBk1dsazsVct520zROiModGfLzs +3sNRSFlGcR+1mwAAAYspIYO6AAAEAwBHMEUCIQDMBLM787qVRefDOcLxP3oPLixI +PVhRhyBsg9XvvohSpQIgCtyZ4oF2QBSfTsCUr/eLF9+QhDozhksmVwxo1OYTKfww +DQYJKoZIhvcNAQELBQADggEBACcFe+By0DsO1nvYF+21jlapEQSyojRcX5h5b5/N +sWxOhi93qLjR7sS3SsffG0WiWBHbsxiAohERPqTvkX1t2TMBJUEyf35PMXyiP3eD +3QEsGzHuofe0VP1niYdpuCcQ1GpaRP0ei5ZUI/g2GcOAf5nETarfz9mi48fo1MyP +Wa0Rg28Yst6t2Av5FDT4dxj4U7eNrzSnSFG9U8Sci9+WocU3gMLpqKA3Yd1MDxRj +OkIYldt8GWmP7W3p0uMW+kAW1WoNR1MxDVfPg2dNKyPJKokoB5CjGPYza7CIOY4J +2tXWqnuMkYbx+xggi3mviBfgI5diodtoCqm4IG5EEkCjTTk= +-----END CERTIFICATE----- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6d2ec07 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.8' +services: + web: + build: . + command: python manage.py runserver_plus 0.0.0.0:8000 + ports: + - 8000:8000 \ No newline at end of file diff --git a/userController/urls.py b/userController/urls.py index f1c63d9..d65f551 100644 --- a/userController/urls.py +++ b/userController/urls.py @@ -3,6 +3,7 @@ # define all routes urlpatterns = [ + path('getTime', views.getTime, name="getTime"), path('checkToken', views.checkToken, name="checkToken"), path('login', views.login, name="login"), path("registerUser", views.registerUser, name="registerUser"), diff --git a/userController/views.py b/userController/views.py index e54dbf6..cfd0651 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,7 +5,7 @@ from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, convertToTime +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -23,6 +23,12 @@ # jwt token expiring time expire_days = 14 +@api_view(['GET']) +@permission_classes([AllowAny]) +def getTime(request): + print(get_client_ip(request)) + return Response(str(datetime.now())) + # will be called every time on open app @csrf_protect @api_view(['POST']) From 38ae1deb7bac676346048f501a2e9555cfc0fbff Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 30 Nov 2023 18:54:03 -0500 Subject: [PATCH 024/107] updated requirements.txt --- requirements.txt | Bin 1836 -> 1906 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1c4c9f2a3d7f9335fa16fb2249622cf9f25c1f06..2fcad14b1b5ef36c9a084593ca64ba16245ccb18 100644 GIT binary patch delta 70 zcmZ3(_la+V46}SXLn%WZLncEqLq0 Date: Fri, 1 Dec 2023 14:05:06 -0500 Subject: [PATCH 025/107] fix cookie not delete --- CCPDController/settings.py | 24 +++++++++++------------- Dockerfile | 6 ------ adminController/views.py | 9 +++++---- certs/ccpowerdeals.ca.crt | 29 ----------------------------- userController/views.py | 6 ++++-- 5 files changed, 20 insertions(+), 54 deletions(-) delete mode 100644 certs/ccpowerdeals.ca.crt diff --git a/CCPDController/settings.py b/CCPDController/settings.py index b2074b3..08c721f 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -82,9 +82,7 @@ # https settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True -SECURE_SSL_REDIRECT = True # http -> https -# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -# SECURE_HSTS_SECONDS = 31536000 +SECURE_SSL_REDIRECT = False # http -> https # cookies settings SESSION_COOKIE_HTTPONLY = True @@ -95,16 +93,16 @@ CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = True # CORS_ALLOWED_ORIGINS = [ - # 'http://172.18.208.1', - # 'https://172.18.208.1', - # "http://localhost", - # "https://localhost", - # "http://142.126.96.24", - # "https://142.126.96.24", - # "http://127.0.0.1:8100", - # "http://127.0.0.1:5173", - # "http://192.168.2.62:8100", - # "http://192.168.2.62:5173", +# 'http://172.18.208.1', +# 'https://172.18.208.1', +# "http://localhost:5173", +# "https://localhost:5173", +# "http://142.126.96.24", +# "https://142.126.96.24", +# "http://127.0.0.1:8100", +# "http://127.0.0.1:5173", +# "http://192.168.2.62:8100", +# "http://192.168.2.62:5173", # ] # csrf settings diff --git a/Dockerfile b/Dockerfile index 99a5685..93b360c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,12 +16,6 @@ RUN pip install -r requirements.txt # copy project COPY . /usr/src/app -# Copy the SSL certificate files into the image -# COPY certs/certificate.pem /usr/local/share/ca-certificates/certificate.pem -# COPY certs/key.pem /usr/local/share/ca-certificates/key.pem -# COPY certs/ccpowerdeals.ca.crt /usr/local/share/ca-certificates/ -# RUN update-ca-certificates - EXPOSE 8000 CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000" ] \ No newline at end of file diff --git a/adminController/views.py b/adminController/views.py index b00f2e6..2f75b38 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -73,14 +73,15 @@ def adminLogin(request): return Response('Login Failed', status.HTTP_404_NOT_FOUND) if bool(user['userActive']) == False: return Response('User Inactive', status.HTTP_401_UNAUTHORIZED) - if (user['role'] != 'Admin'): + if user['role'] != 'Admin': return Response('Permission Denied', status.HTTP_403_FORBIDDEN) try: # construct payload + expire = datetime.utcnow() + timedelta(days=admin_expire_days) payload = { 'id': str(ObjectId(user['_id'])), - 'exp': datetime.utcnow() + timedelta(days=admin_expire_days), + 'exp': expire, 'iat': datetime.utcnow() } @@ -97,8 +98,8 @@ def adminLogin(request): # construct response store jwt token in http only cookie response = Response(info, status.HTTP_200_OK) - response.set_cookie('token', token, httponly=True) - response.set_cookie('csrftoken', get_token(request), httponly=True) + response.set_cookie('token', token, httponly=True, expires=expire, samesite="None", secure=True) + response.set_cookie('csrftoken', get_token(request), httponly=True, expires=expire, samesite="None", secure=True) return response @csrf_protect diff --git a/certs/ccpowerdeals.ca.crt b/certs/ccpowerdeals.ca.crt deleted file mode 100644 index 7c0b13f..0000000 --- a/certs/ccpowerdeals.ca.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE/zCCA+egAwIBAgISAwnfBd881ERWeQ3+NZWvNN3bMA0GCSqGSIb3DQEBCwUA -MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD -EwJSMzAeFw0yMzEwMTMxMjAyMjRaFw0yNDAxMTExMjAyMjNaMBwxGjAYBgNVBAMM -ESouY2Nwb3dlcmRlYWxzLmNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEArYKiu7HBNbLXDA28DuLkY2VGVwYaJ1JPm7ctD8PzKWlZf6g7ezBNK67I7U5+ -qMEhOFyqM3tuEMXGkgwnRh7mAVrDivPbk2dAuFQW6iGkHVE8EEj+XS9F2G7Q31Yl -W0PIuzZkk6Fofb+BECjjfrA0lhkAwjRK0U4+Opo5AyevVLcr+yyWnwWGOSM+n1NI -eZQmS+hvQCSI9DolVbGea/RWg5HsvpaxpHE4Q4MT+rh84ObildnUxLrXCky+c3ZA -9/QN3ex9zAR6OYvat86MF6DS7w1MiupIKbRi6iS0Fa5WbD8uFT/Jt1EBD1Y5qhCe -C7gkEkhRU5ZQ4l5WoInZAwmWNQIDAQABo4ICIzCCAh8wDgYDVR0PAQH/BAQDAgWg -MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0G -A1UdDgQWBBSg0n93nqhbMGYil9skIFgI+RLMnDAfBgNVHSMEGDAWgBQULrMXt1hW -y65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6 -Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iu -b3JnLzAtBgNVHREEJjAkghEqLmNjcG93ZXJkZWFscy5jYYIPY2Nwb3dlcmRlYWxz -LmNhMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDv -AHUAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcAAAGLKSGDwwAABAMA -RjBEAiAIdAavNFbd0hLvHYmQSoFFiygryXJyA7pC44XPN1ytOgIgPR90NWP8Z/fz -7Cebck37xiCxBTX6NdGYvm2i4LNHnnwAdgDuzdBk1dsazsVct520zROiModGfLzs -3sNRSFlGcR+1mwAAAYspIYO6AAAEAwBHMEUCIQDMBLM787qVRefDOcLxP3oPLixI -PVhRhyBsg9XvvohSpQIgCtyZ4oF2QBSfTsCUr/eLF9+QhDozhksmVwxo1OYTKfww -DQYJKoZIhvcNAQELBQADggEBACcFe+By0DsO1nvYF+21jlapEQSyojRcX5h5b5/N -sWxOhi93qLjR7sS3SsffG0WiWBHbsxiAohERPqTvkX1t2TMBJUEyf35PMXyiP3eD -3QEsGzHuofe0VP1niYdpuCcQ1GpaRP0ei5ZUI/g2GcOAf5nETarfz9mi48fo1MyP -Wa0Rg28Yst6t2Av5FDT4dxj4U7eNrzSnSFG9U8Sci9+WocU3gMLpqKA3Yd1MDxRj -OkIYldt8GWmP7W3p0uMW+kAW1WoNR1MxDVfPg2dNKyPJKokoB5CjGPYza7CIOY4J -2tXWqnuMkYbx+xggi3mviBfgI5diodtoCqm4IG5EEkCjTTk= ------END CERTIFICATE----- diff --git a/userController/views.py b/userController/views.py index cfd0651..99fb4f1 100644 --- a/userController/views.py +++ b/userController/views.py @@ -236,8 +236,10 @@ def logout(request): response = Response('User Logout', status.HTTP_200_OK) try: # delete jwt token and csrf token - response.delete_cookie('token', path="/") - response.delete_cookie('csrftoken', path="/") + # response.delete_cookie('token', path="/") + # response.delete_cookie('csrftoken', path="/") + response.set_cookie('token', expires=0, max_age=0, secure=True, samesite='none') + response.set_cookie('csrftoken', expires=0, max_age=0, secure=True, samesite='none') except: return Response('Token Not Found', status.HTTP_404_NOT_FOUND) return response \ No newline at end of file From dcbdc10eac527c592accf84dd9130a9c4cdf9133 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 1 Dec 2023 18:55:10 -0500 Subject: [PATCH 026/107] update admin route --- adminController/urls.py | 3 ++- adminController/views.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/adminController/urls.py b/adminController/urls.py index ba310ae..4f3e60d 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -8,5 +8,6 @@ path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('setUserActive', views.setUserActive, name="setUserActive"), path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), - path('getAllInventory', views.getAllInventory, name="getAllInventory") + path('getAllInventory', views.getAllInventory, name="getAllInventory"), + path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo") ] diff --git a/adminController/views.py b/adminController/views.py index 2f75b38..9c63760 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -224,4 +224,14 @@ def getAllInventory(request): for item in inventory_collection.find({}, {'_id': 0}): inv.append(item) - return Response(inv, status.HTTP_200_OK) \ No newline at end of file + return Response(inv, status.HTTP_200_OK) + +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getAllUserInfo(request): + arr = [] + for item in inventory_collection.find({}, {'_id': 0, 'password': 0 }): + arr.push(item) + + return Response(arr, status.HTTP_200_OK) \ No newline at end of file From 1fe16c4b8f8290769627333006accd23a1d3b0f7 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Mon, 4 Dec 2023 19:02:40 -0500 Subject: [PATCH 027/107] updated routes --- CCPDController/permissions.py | 8 ++- CCPDController/utils.py | 13 ++++- Dockerfile | 4 +- adminController/models.py | 4 +- adminController/urls.py | 5 +- adminController/views.py | 99 ++++++++++++++++++++--------------- docker-compose.yml | 5 +- userController/views.py | 35 +++++++------ 8 files changed, 107 insertions(+), 66 deletions(-) diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py index 0888d2c..71c661a 100644 --- a/CCPDController/permissions.py +++ b/CCPDController/permissions.py @@ -20,7 +20,7 @@ class IsQAPermission(permissions.BasePermission): def has_permission(self, request, view): # mongo db query # grant if user is qa personal and user is active - if request.auth == 'QAPersonal' and request.user['userActive'] == True: + if request.auth == 'QAPersonal' and bool(request.user['userActive']) == True: return True # admin permission @@ -30,6 +30,12 @@ def has_permission(self, request, view): if request.auth == 'Admin' and bool(request.user['userActive']) == True : return True +class IsSuperAdmin(permissions.BasePermission): + message = 'Permission Denied, Super Admin Only!' + def has_permission(self, request, view): + if request.auth == 'SAdmin': + return True + # user blocked by IP black list class BlockedPermission(permissions.BasePermission): message = 'You Are Blocked From Our Service' diff --git a/CCPDController/utils.py b/CCPDController/utils.py index ede2487..ac34344 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -31,7 +31,7 @@ def get_client_ip(request): return ip # limit variables -max_name = 40 +max_name = 50 min_name = 3 max_email = 45 min_email = 6 @@ -124,8 +124,17 @@ def sanitizePassword(password): def sanitizePlatform(platform): if platform not in ['Amazon', 'eBay', 'Official Website', 'Other']: return False + return platform # shelf location sanitize def sanitizeShelfLocation(shelfLocation): if not isinstance(shelfLocation, str): - return False \ No newline at end of file + return False + return shelfLocation + +def sanitizeInvitationCode(code): + if not isinstance(code, str): + return False + if not inRange(code, min_inv_code, max_inv_code): + return False + return code \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 93b360c..78b84a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,6 @@ COPY . /usr/src/app EXPOSE 8000 -CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000" ] \ No newline at end of file +CMD [ "python", "manage.py", "runserver_plus", "0.0.0.0:8000" ] + +# CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi:application"] diff --git a/adminController/models.py b/adminController/models.py index 5bb2fb1..ecfc35c 100644 --- a/adminController/models.py +++ b/adminController/models.py @@ -2,13 +2,11 @@ class InvitationCode(models.Model): code: models.CharField(max_length=100) - available: models.BooleanField(max_length=5) exp: models.CharField(max_length=25) # constructor input all info - def __init__(self, code, available, exp) -> None: + def __init__(self, code, exp) -> None: self.code = code - self.available = available self.exp = exp # return inventory sku diff --git a/adminController/urls.py b/adminController/urls.py index 4f3e60d..9c30b14 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -8,6 +8,9 @@ path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('setUserActive', views.setUserActive, name="setUserActive"), path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), + path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), path('getAllInventory', views.getAllInventory, name="getAllInventory"), - path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo") + path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo"), + path('getAllInvitationCode', views.getAllInvitationCode, name="getAllInvitationCode"), + path('deleteInvitationCode', views.deleteInvitationCode, name="deleteInvitationCode"), ] diff --git a/adminController/views.py b/adminController/views.py index 9c63760..77555b1 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -1,17 +1,16 @@ import jwt import uuid -import json from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_protect from django.middleware.csrf import get_token from bson.objectid import ObjectId -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from .models import InvitationCode from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny +from rest_framework.permissions import IsAdminUser, AllowAny from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication @@ -21,7 +20,7 @@ db = get_db_client() user_collection = db['User'] inventory_collection = db['Inventory'] -inv_collection = db['Invitations'] +inv_code_collection = db['Invitations'] # admin jwt token expiring time admin_expire_days = 90 @@ -158,35 +157,8 @@ def setUserActive(request): return Response('Updated User Activation Status', status.HTTP_200_OK) return Response('User Not Found') -# admin generate invitation code for newly hired QA personal to join -@api_view(['POST']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def issueInvitationCode(request): - - - # generate a uuid for invitation code - inviteCode = uuid.uuid4() - print(inviteCode) - - expireTime = datetime.now() - - - newCode = InvitationCode( - code = inviteCode, - available = True, - exp = '', - ) - - try: - inv_collection.insert_one(newCode) - except: - return Response('Database Error', status.HTTP_500_INTERNAL_SERVER_ERROR) - - return Response('Invitation Code Created: '.join(inviteCode), status.HTTP_200_OK) - # update anyones password by id -# _id: string +# id: string # newpassword: string @api_view(['PUT']) @authentication_classes([JWTAuthentication]) @@ -195,12 +167,12 @@ def updatePasswordById(request): try: # if failed to convert to BSON response 401 body = decodeJSON(request.body) - uid = ObjectId(body['_id']) + uid = ObjectId(body['id']) except: return Response('User ID Invalid:', status.HTTP_400_BAD_REQUEST) # query db for user - res = user_collection.find_one({ '_id': uid }) + res = user_collection.find_one({ 'id': uid }) # check if password is valid if not sanitizePassword(body['password']): @@ -209,7 +181,7 @@ def updatePasswordById(request): # if found, change its pass word if res: user_collection.update_one( - { '_id': uid }, + { 'id': uid }, { '$set': {'password': body['password']} } ) return Response('Password Updated', status.HTTP_200_OK) @@ -223,15 +195,58 @@ def getAllInventory(request): inv = [] for item in inventory_collection.find({}, {'_id': 0}): inv.append(item) - return Response(inv, status.HTTP_200_OK) @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) -def getAllUserInfo(request): - arr = [] - for item in inventory_collection.find({}, {'_id': 0, 'password': 0 }): - arr.push(item) - - return Response(arr, status.HTTP_200_OK) \ No newline at end of file +def getAllUserInfo(request): + userArr = [] + for item in user_collection.find({}, {'password': 0, '_id': 0}): + userArr.append(item) + return Response(userArr, status.HTTP_200_OK) + +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getAllInvitationCode(request): + codeArr = [] + for item in inv_code_collection.find({}, {'_id': 0}): + codeArr.append(item) + return Response(codeArr, status.HTTP_200_OK) + +# admin generate invitation code for newly hired QA personal to join +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def issueInvitationCode(request): + # generate a uuid for invitation code + inviteCode = uuid.uuid4() + expireTime = datetime.now() + timedelta(days=1) + newCode = InvitationCode( + code = str(inviteCode), + exp = expireTime, + ) + + try: + res = inv_code_collection.insert_one(newCode.__dict__) + except: + return Response('Server Error', status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response('Invitation Code Created', status.HTTP_200_OK) + +@api_view(['DELETE']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def deleteInvitationCode(request): + try: + body = decodeJSON(request.body) + code = body['code'] + except: + return Response('Invalid Body: ', status.HTTP_400_BAD_REQUEST) + + try: + res = inv_code_collection.delete_one({'code': code}) + except: + return Response('Delete Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('Code Deleted!', status.HTTP_200_OK) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6d2ec07..be559f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,10 @@ version: '3.8' services: web: - build: . + build: + context: . + dockerfile: ./Dockerfile + image: cccrizzz/ccpd-django-service command: python manage.py runserver_plus 0.0.0.0:8000 ports: - 8000:8000 \ No newline at end of file diff --git a/userController/views.py b/userController/views.py index 99fb4f1..6cb7add 100644 --- a/userController/views.py +++ b/userController/views.py @@ -5,7 +5,7 @@ from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip, sanitizeInvitationCode, sanitizeName from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -156,37 +156,44 @@ def getUserById(request): @csrf_protect @api_view(['POST']) def registerUser(request): + body = checkBody(decodeJSON(request.body)) try: # sanitize - body = checkBody(decodeJSON(request.body)) email = sanitizeEmail(body['email']) - pwd = sanitizePassword(body['password']) - invCode = body['code'] + userName = sanitizeName(body['name']) + password = sanitizePassword(body['password']) + invCode = sanitizeInvitationCode(body['code']) + print(email) + print(userName) + print(password) + print(invCode) # check if email exist in database res = collection.find_one({ 'email': body['email'] }) if res: return Response('Email already existed!', status.HTTP_409_CONFLICT) + if email == False or password == False or invCode == False: + return Response('Invalid Registration Info', status.HTTP_400_BAD_REQUEST) except: - return Response('Invalid Registration Info', status.HTTP_400_BAD_REQUEST) - - if email == False or pwd == False: - return Response('Invalid Email Or Password', status.HTTP_400_BAD_REQUEST) + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) # check if admin issues such code code = inv_collection.find_one({'code': invCode}) if not code: - return Response('Invitation Code Not Found', status.HTTP_404_NOT_FOUND) + return Response('Invilid Invitation Code', status.HTTP_404_NOT_FOUND) + + print(code) + print((code['exp'])) - # # check if token expired + # check if token expired # expTime = convertToTime(body['exp']) # if ((expTime - datetime.now()).total_seconds() < 0): # return Response('Invitation Code Expired', status.HTTP_406_NOT_ACCEPTABLE) # construct user newUser = User( - name=body['name'], - email=body['email'], - password=body['password'], + name=userName, + email=email, + password=password, role='QAPersonal', registrationDate=date.today().isoformat(), userActive=True @@ -236,8 +243,6 @@ def logout(request): response = Response('User Logout', status.HTTP_200_OK) try: # delete jwt token and csrf token - # response.delete_cookie('token', path="/") - # response.delete_cookie('csrftoken', path="/") response.set_cookie('token', expires=0, max_age=0, secure=True, samesite='none') response.set_cookie('csrftoken', expires=0, max_age=0, secure=True, samesite='none') except: From f7126c8824c0df539923d451a83a1672bdd66866 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 6 Dec 2023 19:00:04 -0500 Subject: [PATCH 028/107] fixed timestamp problem --- adminController/views.py | 3 +-- userController/views.py | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adminController/views.py b/adminController/views.py index 77555b1..ede2198 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -222,7 +222,7 @@ def getAllInvitationCode(request): def issueInvitationCode(request): # generate a uuid for invitation code inviteCode = uuid.uuid4() - expireTime = datetime.now() + timedelta(days=1) + expireTime = (datetime.now() + timedelta(days=1)).timestamp() newCode = InvitationCode( code = str(inviteCode), exp = expireTime, @@ -232,7 +232,6 @@ def issueInvitationCode(request): res = inv_code_collection.insert_one(newCode.__dict__) except: return Response('Server Error', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response('Invitation Code Created', status.HTTP_200_OK) @api_view(['DELETE']) diff --git a/userController/views.py b/userController/views.py index 6cb7add..e0fb675 100644 --- a/userController/views.py +++ b/userController/views.py @@ -178,11 +178,11 @@ def registerUser(request): # check if admin issues such code code = inv_collection.find_one({'code': invCode}) - if not code: - return Response('Invilid Invitation Code', status.HTTP_404_NOT_FOUND) + if not code: #or expired: + return Response('Code invalid', status.HTTP_404_NOT_FOUND) - print(code) - print((code['exp'])) + print(code['exp']) + print(datetime.now().timestamp() - code['exp']) # check if token expired # expTime = convertToTime(body['exp']) @@ -203,6 +203,7 @@ def registerUser(request): res = collection.insert_one(newUser.__dict__) if res: + inv_collection.delete_one({'code': invCode}) return Response('Registration Successful', status.HTTP_200_OK) return Response('Registration Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) From 99765b3d835ab64cacb2ffe23297f1c034b889e3 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 7 Dec 2023 18:56:54 -0500 Subject: [PATCH 029/107] updated admin controller --- CCPDController/settings.py | 26 +++++++------- adminController/urls.py | 2 +- adminController/views.py | 74 +++++++++++++++++++++++++++++++++----- userController/models.py | 8 +++-- userController/views.py | 5 +-- 5 files changed, 87 insertions(+), 28 deletions(-) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 08c721f..b2560ab 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -93,23 +93,25 @@ CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = True # CORS_ALLOWED_ORIGINS = [ -# 'http://172.18.208.1', -# 'https://172.18.208.1', -# "http://localhost:5173", -# "https://localhost:5173", -# "http://142.126.96.24", -# "https://142.126.96.24", -# "http://127.0.0.1:8100", -# "http://127.0.0.1:5173", -# "http://192.168.2.62:8100", -# "http://192.168.2.62:5173", + # "http://192.168.56.1:8100", + # "http://192.168.56.1:5173", + # 'http://172.18.208.1', + # 'https://172.18.208.1', + # "http://localhost:5173", + # "https://localhost:5173", + # "http://142.126.96.24", + # "https://142.126.96.24", + # "http://127.0.0.1:8100", + # "http://127.0.0.1:5173", + # "http://192.168.2.62:8100", + # "http://192.168.2.62:5173", # ] # csrf settings CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = [ - 'http://172.18.208.1', - 'https://172.18.208.1', + # 'http://172.18.208.1', + # 'https://172.18.208.1', # "http://localhost", # "https://localhost", # "http://142.126.96.24", diff --git a/adminController/urls.py b/adminController/urls.py index 9c30b14..755d5e6 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -6,7 +6,7 @@ path('checkAdminToken', views.checkAdminToken, name='checkAdminToken'), path('adminLogin', views.adminLogin, name='adminLogin'), path('deleteUserById', views.deleteUserById, name="deleteUserById"), - path('setUserActive', views.setUserActive, name="setUserActive"), + path('setUserActiveById', views.setUserActiveById, name="setUserActiveById"), path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), path('getAllInventory', views.getAllInventory, name="getAllInventory"), diff --git a/adminController/views.py b/adminController/views.py index ede2198..4072072 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -6,6 +6,7 @@ from django.middleware.csrf import get_token from bson.objectid import ObjectId from datetime import datetime, timedelta +from userController.models import User from .models import InvitationCode from rest_framework import status from rest_framework.response import Response @@ -14,7 +15,7 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeName # pymongo db = get_db_client() @@ -108,7 +109,7 @@ def registerAdmin(request): return Response('Registration Success') # delete user by id -# _id: string +# id: string @api_view(['DELETE']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) @@ -116,7 +117,7 @@ def deleteUserById(request): try: # convert to BSON body = decodeJSON(request.body) - uid = ObjectId(body['_id']) + uid = ObjectId(body['id']) except: return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) @@ -130,17 +131,16 @@ def deleteUserById(request): return Response('User Not Found', status.HTTP_404_NOT_FOUND) # set any user status to be active or disabled -# _id: string, -# password: string +# id: string, # userActive: bool @api_view(['PUT']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) -def setUserActive(request): +def setUserActiveById(request): try: # convert to BSON body = decodeJSON(request.body) - uid = ObjectId(body['_id']) + uid = ObjectId(body['id']) except: return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) @@ -188,6 +188,63 @@ def updatePasswordById(request): return Response('User Not Found', status.HTTP_404_NOT_FOUND) +# update user information by id +# id: string +# body: UserDetail +@api_view(['PUT']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def updateUserById(request, uid): + try: + # if failed to convert to BSON response 401 + body = decodeJSON(request.body) + userId = ObjectId(uid) + + # if user not in db throw 404 + res = user_collection.find_one({ 'id': userId }) + if not res: + return Response('User Not Found', status.HTTP_404_NOT_FOUND) + + # remove all "$" and ensure no object {} passed in here + email = sanitizeEmail(body['email']) + name = sanitizeName(body['name']) + password = sanitizePassword(body['password']) + + if email == False or name == False or password == False: + return Response('User Info Invalid', status.HTTP_400_BAD_REQUEST) + + # run new info through object relational mapping + newUserInfo = User ( + name=name, + email=email, + role=body['role'], + password=password, + registrationDate=res['registrationDate'], + userActive=body['userActive'] + ) + except: + return Response('User Info Invalid:', status.HTTP_400_BAD_REQUEST) + + try: + # if found, update its info + user_collection.update_one( + { 'id': userId }, + { + '$set': { + 'name': newUserInfo['name'], + 'email': newUserInfo['email'], + 'role': newUserInfo['role'], + 'password': newUserInfo['password'], + 'registrationDate': newUserInfo['registrationDate'], + 'userActive': newUserInfo['userActive'] + } + } + ) + except: + return Response('Update User Info Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('Password Updated', status.HTTP_200_OK) + + @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) @@ -202,7 +259,8 @@ def getAllInventory(request): @permission_classes([IsAdminPermission]) def getAllUserInfo(request): userArr = [] - for item in user_collection.find({}, {'password': 0, '_id': 0}): + for item in user_collection.find({}, {'password': 0}): + item['_id'] = str(item['_id']) userArr.append(item) return Response(userArr, status.HTTP_200_OK) diff --git a/userController/models.py b/userController/models.py index 5a5fb0e..79802c0 100644 --- a/userController/models.py +++ b/userController/models.py @@ -4,14 +4,16 @@ class User(models.Model): ROLE_CHOISES = [ ('Admin', 'Admin'), + ('Super Admin', 'Super Admin'), ('QAPersonal', 'QAPersonal'), + ('Sales', 'Sales'), ] _id: models.AutoField(primary_key=True) - name: models.CharField(max_length=20, validators=[MinLengthValidator(3, 'Name Invalid')]) + name: models.CharField(max_length=40, validators=[MinLengthValidator(3, 'Name Invalid')]) email: models.EmailField(max_length=45, validators=[MinLengthValidator(8, 'Email Invalid')]) - password: models.CharField(max_length=45, validators=[MinLengthValidator(8, 'Password Invalid')]) - role: models.CharField(max_length=12, validators=[MinLengthValidator(4, 'Role Invalid')], choices=ROLE_CHOISES) + password: models.CharField(max_length=50, validators=[MinLengthValidator(8, 'Password Invalid')]) + role: models.CharField(max_length=20, validators=[MinLengthValidator(4, 'Role Invalid')], choices=ROLE_CHOISES) registrationDate: models.CharField(max_length=30, validators=[MinLengthValidator(8,'Registration date invalid')]) userActive: models.BooleanField() diff --git a/userController/views.py b/userController/views.py index e0fb675..a3c7daa 100644 --- a/userController/views.py +++ b/userController/views.py @@ -163,10 +163,7 @@ def registerUser(request): userName = sanitizeName(body['name']) password = sanitizePassword(body['password']) invCode = sanitizeInvitationCode(body['code']) - print(email) - print(userName) - print(password) - print(invCode) + # check if email exist in database res = collection.find_one({ 'email': body['email'] }) if res: From 667be2ab19091665c63a8ef2fc1f73dcedcce430 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 8 Dec 2023 19:03:53 -0500 Subject: [PATCH 030/107] working on admin controller --- CCPDController/utils.py | 1 + adminController/urls.py | 1 + adminController/views.py | 101 ++++++++++++++++++++++----------------- userController/views.py | 16 +++---- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index ac34344..6fd25f4 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -132,6 +132,7 @@ def sanitizeShelfLocation(shelfLocation): return False return shelfLocation +# invitation code should be a string def sanitizeInvitationCode(code): if not isinstance(code, str): return False diff --git a/adminController/urls.py b/adminController/urls.py index 755d5e6..76f1ce4 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -8,6 +8,7 @@ path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('setUserActiveById', views.setUserActiveById, name="setUserActiveById"), path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), + path('updateUserById/', views.updateUserById, name="updateUserById"), path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), path('getAllInventory', views.getAllInventory, name="getAllInventory"), path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo"), diff --git a/adminController/views.py b/adminController/views.py index 4072072..fb2acd3 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -195,53 +195,68 @@ def updatePasswordById(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def updateUserById(request, uid): - try: - # if failed to convert to BSON response 401 - body = decodeJSON(request.body) - userId = ObjectId(uid) + # try: + # if failed to convert to BSON response 401 + body = decodeJSON(request.body) + userId = ObjectId(uid) - # if user not in db throw 404 - res = user_collection.find_one({ 'id': userId }) - if not res: - return Response('User Not Found', status.HTTP_404_NOT_FOUND) - - # remove all "$" and ensure no object {} passed in here - email = sanitizeEmail(body['email']) - name = sanitizeName(body['name']) - password = sanitizePassword(body['password']) - - if email == False or name == False or password == False: - return Response('User Info Invalid', status.HTTP_400_BAD_REQUEST) - - # run new info through object relational mapping - newUserInfo = User ( - name=name, - email=email, - role=body['role'], - password=password, - registrationDate=res['registrationDate'], - userActive=body['userActive'] - ) - except: - return Response('User Info Invalid:', status.HTTP_400_BAD_REQUEST) + # if user not in db throw 404 + res = user_collection.find_one({ '_id': userId }) + if not res: + return Response('User Not Found', status.HTTP_404_NOT_FOUND) + # if password not passed in, use the old pass for the new object try: - # if found, update its info - user_collection.update_one( - { 'id': userId }, - { - '$set': { - 'name': newUserInfo['name'], - 'email': newUserInfo['email'], - 'role': newUserInfo['role'], - 'password': newUserInfo['password'], - 'registrationDate': newUserInfo['registrationDate'], - 'userActive': newUserInfo['userActive'] - } + body['password'] = sanitizePassword(body['password']) + except: + body['password'] = res['password'] + + # remove all "$" and ensure no object {} passed in here + body['email'] = sanitizeEmail(body['email']) + body['name'] = sanitizeName(body['name']) + + print('user before modification:') + print(res) + print('req body:') + print(body) + + print(body['email']) + print(body['name']) + + if body['email'] == False or body['name'] == False or body['password'] == False: + return Response('User Info Invalid', status.HTTP_400_BAD_REQUEST) + + # run new info through object relational mapping + newUserInfo = User ( + name=body['name'], + email=body['email'], + role=body['role'], + password=body['password'], + registrationDate=res['registrationDate'], + userActive=body['userActive'] + ) + # except: + # return Response('User Info Invalid:', status.HTTP_400_BAD_REQUEST) + + # try: + # if found, update its info + print(newUserInfo) + + user_collection.update_one( + { 'id': userId }, + { + '$set': { + 'name': newUserInfo.name, + 'email': newUserInfo.email, + 'role': newUserInfo.role, + 'password': newUserInfo.password, + 'registrationDate': newUserInfo.registrationDate, + 'userActive': newUserInfo.userActive } - ) - except: - return Response('Update User Info Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) + } + ) + # except: + # return Response('Update User Info Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response('Password Updated', status.HTTP_200_OK) diff --git a/userController/views.py b/userController/views.py index a3c7daa..a4130a1 100644 --- a/userController/views.py +++ b/userController/views.py @@ -1,3 +1,4 @@ +import time import jwt from django.conf import settings from django.shortcuts import HttpResponse @@ -10,7 +11,7 @@ from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import AllowAny -from rest_framework.exceptions import AuthenticationFailed, PermissionDenied +from rest_framework.exceptions import AuthenticationFailed from rest_framework import status from rest_framework.response import Response from userController.models import User @@ -173,18 +174,17 @@ def registerUser(request): except: return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) - # check if admin issues such code + # check if code is in db code = inv_collection.find_one({'code': invCode}) - if not code: #or expired: + if not code: return Response('Code invalid', status.HTTP_404_NOT_FOUND) - print(code['exp']) - print(datetime.now().timestamp() - code['exp']) + # get today's unix float + today = time.mktime(datetime.now().timetuple()) # check if token expired - # expTime = convertToTime(body['exp']) - # if ((expTime - datetime.now()).total_seconds() < 0): - # return Response('Invitation Code Expired', status.HTTP_406_NOT_ACCEPTABLE) + if (bool(code['exp'] - today < 0)): + return Response('Invitation Code Expired', status.HTTP_410_GONE) # construct user newUser = User( From 3f6704dc5fd0bcf75aa0cc33251014dc4c84f9a7 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 12 Dec 2023 19:01:51 -0500 Subject: [PATCH 031/107] fixed admin route --- CCPDController/utils.py | 33 ++++++++-- adminController/urls.py | 2 +- adminController/views.py | 128 ++++++++++++--------------------------- userController/views.py | 2 +- 4 files changed, 67 insertions(+), 98 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 6fd25f4..f7727a1 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -41,6 +41,8 @@ def get_client_ip(request): min_sku = 4 max_inv_code = 100 min_inv_code = 10 +max_role = 12 +min_role = 4 # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 @@ -48,11 +50,6 @@ def get_client_ip(request): def convertToTime(time_str): return datetime.strptime(time_str, time_format) -def sanitizeInvCode(code): - if not isinstance(code, str): - return False - return code - # check if body contains valid user registration information def checkBody(body): if not inRange(body['name'], min_name, max_name): @@ -101,6 +98,14 @@ def sanitizeName(name): return False return clean_name +# role length from 4 to 12 +def sanitizeRole(role): + if not isinstance(role, str): + return False + if not inRange(role, min_role, max_role): + return False + return role + # email can be from 7 chars to 40 chars def sanitizeEmail(email): # type and format check @@ -138,4 +143,20 @@ def sanitizeInvitationCode(code): return False if not inRange(code, min_inv_code, max_inv_code): return False - return code \ No newline at end of file + return code + +# make sure string is type str and no $ included +def sanitizeString(field): + if not isinstance(field, str): + raise TypeError('Invalid Information') + return field.replace('$', '') + +# sanitize all field in user info body +def sanitizeBody(body): + for attr, value in body.items(): + # if hit user active field set the field to bool type + # if not sanitize string and remove '$' + if attr == 'userActive': + body[attr] = bool(value == 'true') + else: + body[attr] = sanitizeString(value) \ No newline at end of file diff --git a/adminController/urls.py b/adminController/urls.py index 76f1ce4..450c8e7 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -5,9 +5,9 @@ urlpatterns = [ path('checkAdminToken', views.checkAdminToken, name='checkAdminToken'), path('adminLogin', views.adminLogin, name='adminLogin'), + path('createUser', views.createUser, name='createUser'), path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('setUserActiveById', views.setUserActiveById, name="setUserActiveById"), - path('updatePasswordById', views.updatePasswordById, name="updatePasswordById"), path('updateUserById/', views.updateUserById, name="updateUserById"), path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), path('getAllInventory', views.getAllInventory, name="getAllInventory"), diff --git a/adminController/views.py b/adminController/views.py index fb2acd3..16bd45e 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -15,7 +15,7 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeName +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody # pymongo db = get_db_client() @@ -104,9 +104,20 @@ def adminLogin(request): @csrf_protect @api_view(['POST']) -@permission_classes([AllowAny]) -def registerAdmin(request): - return Response('Registration Success') +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def createUser(request): + try: + body = decodeJSON(request.body) + sanitizeBody(body) + except: + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) + + print(body) + # res = user_collection.insert_one(body) + + + return Response('User Created') # delete user by id # id: string @@ -157,37 +168,6 @@ def setUserActiveById(request): return Response('Updated User Activation Status', status.HTTP_200_OK) return Response('User Not Found') -# update anyones password by id -# id: string -# newpassword: string -@api_view(['PUT']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def updatePasswordById(request): - try: - # if failed to convert to BSON response 401 - body = decodeJSON(request.body) - uid = ObjectId(body['id']) - except: - return Response('User ID Invalid:', status.HTTP_400_BAD_REQUEST) - - # query db for user - res = user_collection.find_one({ 'id': uid }) - - # check if password is valid - if not sanitizePassword(body['password']): - return Response('Invalid Password', status.HTTP_400_BAD_REQUEST) - - # if found, change its pass word - if res: - user_collection.update_one( - { 'id': uid }, - { '$set': {'password': body['password']} } - ) - return Response('Password Updated', status.HTTP_200_OK) - return Response('User Not Found', status.HTTP_404_NOT_FOUND) - - # update user information by id # id: string # body: UserDetail @@ -195,68 +175,36 @@ def updatePasswordById(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def updateUserById(request, uid): - # try: - # if failed to convert to BSON response 401 - body = decodeJSON(request.body) - userId = ObjectId(uid) - - # if user not in db throw 404 - res = user_collection.find_one({ '_id': userId }) - if not res: - return Response('User Not Found', status.HTTP_404_NOT_FOUND) - - # if password not passed in, use the old pass for the new object try: - body['password'] = sanitizePassword(body['password']) - except: - body['password'] = res['password'] + # convert string to ObjectId + userId = ObjectId(uid) + + # if user not in db throw 404 + user = user_collection.find_one({ '_id': userId }) + if not user: + return Response('User Not Found', status.HTTP_404_NOT_FOUND) + body = decodeJSON(request.body) + + # loop body obeject and remove $ + sanitizeBody(body) + except: + return Response('Invalid User Info', status.HTTP_400_BAD_REQUEST) - # remove all "$" and ensure no object {} passed in here - body['email'] = sanitizeEmail(body['email']) - body['name'] = sanitizeName(body['name']) - print('user before modification:') - print(res) + print(user) print('req body:') print(body) - print(body['email']) - print(body['name']) - - if body['email'] == False or body['name'] == False or body['password'] == False: - return Response('User Info Invalid', status.HTTP_400_BAD_REQUEST) - - # run new info through object relational mapping - newUserInfo = User ( - name=body['name'], - email=body['email'], - role=body['role'], - password=body['password'], - registrationDate=res['registrationDate'], - userActive=body['userActive'] - ) - # except: - # return Response('User Info Invalid:', status.HTTP_400_BAD_REQUEST) - - # try: - # if found, update its info - print(newUserInfo) - - user_collection.update_one( - { 'id': userId }, - { - '$set': { - 'name': newUserInfo.name, - 'email': newUserInfo.email, - 'role': newUserInfo.role, - 'password': newUserInfo.password, - 'registrationDate': newUserInfo.registrationDate, - 'userActive': newUserInfo.userActive + # update user information + try: + user_collection.update_one( + { '_id': userId }, + { + '$set': body } - } - ) - # except: - # return Response('Update User Info Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) + ) + except: + return Response('Update User Infomation Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response('Password Updated', status.HTTP_200_OK) diff --git a/userController/views.py b/userController/views.py index a4130a1..73250ae 100644 --- a/userController/views.py +++ b/userController/views.py @@ -22,7 +22,7 @@ inv_collection = db['Invitations'] # jwt token expiring time -expire_days = 14 +expire_days = 30 @api_view(['GET']) @permission_classes([AllowAny]) From a56bb4f944aca7c7c9e818887e95e9a53329abc1 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 13 Dec 2023 18:57:51 -0500 Subject: [PATCH 032/107] fixed date problem --- CCPDController/utils.py | 6 ++++- adminController/urls.py | 1 - adminController/views.py | 49 ++++++++++++------------------------ inventoryController/views.py | 1 + userController/models.py | 1 + userController/views.py | 6 ++--- 6 files changed, 26 insertions(+), 38 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index f7727a1..4946cd4 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -44,9 +44,12 @@ def get_client_ip(request): max_role = 12 min_role = 4 +# user format +user_time_format = "%b %-d %Y" +# inventory and invitation code format +time_format = "%a %b %d %H:%M:%S %Y" # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 -time_format = "%a %b %d %H:%M:%S %Y" def convertToTime(time_str): return datetime.strptime(time_str, time_format) @@ -152,6 +155,7 @@ def sanitizeString(field): return field.replace('$', '') # sanitize all field in user info body +# make sure user is active and remove $ def sanitizeBody(body): for attr, value in body.items(): # if hit user active field set the field to bool type diff --git a/adminController/urls.py b/adminController/urls.py index 450c8e7..f23be8b 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -7,7 +7,6 @@ path('adminLogin', views.adminLogin, name='adminLogin'), path('createUser', views.createUser, name='createUser'), path('deleteUserById', views.deleteUserById, name="deleteUserById"), - path('setUserActiveById', views.setUserActiveById, name="setUserActiveById"), path('updateUserById/', views.updateUserById, name="updateUserById"), path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), path('getAllInventory', views.getAllInventory, name="getAllInventory"), diff --git a/adminController/views.py b/adminController/views.py index 16bd45e..648396e 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -5,7 +5,7 @@ from django.views.decorators.csrf import csrf_protect from django.middleware.csrf import get_token from bson.objectid import ObjectId -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from userController.models import User from .models import InvitationCode from rest_framework import status @@ -15,7 +15,7 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody, user_time_format # pymongo db = get_db_client() @@ -110,14 +110,24 @@ def createUser(request): try: body = decodeJSON(request.body) sanitizeBody(body) + newUser = User ( + name=body['name'], + email=body['email'], + password=body['password'], + role=body['role'], + registrationDate=date.today().strftime(user_time_format), + userActive=True + ) except: return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) - print(body) - # res = user_collection.insert_one(body) - + print(newUser) - return Response('User Created') + try: + user_collection.insert_one(newUser.__dict__) + except: + return Response('Unable to Create User', status.HTTP_400_BAD_REQUEST) + return Response('User Created', status.HTTP_201_CREATED) # delete user by id # id: string @@ -141,33 +151,6 @@ def deleteUserById(request): return Response('User Deleted', status.HTTP_200_OK) return Response('User Not Found', status.HTTP_404_NOT_FOUND) -# set any user status to be active or disabled -# id: string, -# userActive: bool -@api_view(['PUT']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def setUserActiveById(request): - try: - # convert to BSON - body = decodeJSON(request.body) - uid = ObjectId(body['id']) - except: - return Response('Invalid User ID', status.HTTP_400_BAD_REQUEST) - - # query db for user - res = user_collection.find_one({'_id': uid}) - - # if found, switch user active to false - if res : - res = user_collection.update_one( - { '_id': uid }, - { '$set': {'userActive': body['userActive']} } - ) - if res: - return Response('Updated User Activation Status', status.HTTP_200_OK) - return Response('User Not Found') - # update user information by id # id: string # body: UserDetail diff --git a/inventoryController/views.py b/inventoryController/views.py index cd22b55..7706f6d 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -74,6 +74,7 @@ def getInventoryByOwnerId(request, page): return Response(arr, status.HTTP_200_OK) +# for charts and overview data # id: string @api_view(['POST']) @authentication_classes([JWTAuthentication]) diff --git a/userController/models.py b/userController/models.py index 79802c0..ffa5d12 100644 --- a/userController/models.py +++ b/userController/models.py @@ -7,6 +7,7 @@ class User(models.Model): ('Super Admin', 'Super Admin'), ('QAPersonal', 'QAPersonal'), ('Sales', 'Sales'), + ('Shelving Manager', 'Shelving Manager'), ] _id: models.AutoField(primary_key=True) diff --git a/userController/views.py b/userController/views.py index 73250ae..578b5cb 100644 --- a/userController/views.py +++ b/userController/views.py @@ -6,7 +6,7 @@ from django.middleware.csrf import get_token from datetime import date, datetime, timedelta from bson.objectid import ObjectId -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip, sanitizeInvitationCode, sanitizeName +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, checkBody, get_client_ip, sanitizeInvitationCode, sanitizeName, user_time_format from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -192,7 +192,7 @@ def registerUser(request): email=email, password=password, role='QAPersonal', - registrationDate=date.today().isoformat(), + registrationDate=date.today().strftime(user_time_format), userActive=True ) @@ -201,7 +201,7 @@ def registerUser(request): if res: inv_collection.delete_one({'code': invCode}) - return Response('Registration Successful', status.HTTP_200_OK) + return Response('Registration Successful', status.HTTP_201_CREATED) return Response('Registration Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) # QA personal change own password From 205102a47858f6993cfa457859acbff9257bae45 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 14 Dec 2023 18:56:12 -0500 Subject: [PATCH 033/107] added QARecords function --- CCPDController/utils.py | 5 +++++ adminController/urls.py | 1 + adminController/views.py | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 4946cd4..da15335 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -154,6 +154,11 @@ def sanitizeString(field): raise TypeError('Invalid Information') return field.replace('$', '') +# makesure number is int and no $ +def sanitizeNumber(num): + if not isinstance(num, int): + raise TypeError('Invalid Information') + # sanitize all field in user info body # make sure user is active and remove $ def sanitizeBody(body): diff --git a/adminController/urls.py b/adminController/urls.py index f23be8b..3ff5d31 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -13,4 +13,5 @@ path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo"), path('getAllInvitationCode', views.getAllInvitationCode, name="getAllInvitationCode"), path('deleteInvitationCode', views.deleteInvitationCode, name="deleteInvitationCode"), + path('getQARecordsByPage', views.getQARecordsByPage, name="getQARecordsByPage") ] diff --git a/adminController/views.py b/adminController/views.py index 648396e..606e175 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -1,5 +1,6 @@ import jwt import uuid +import pymongo from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_protect @@ -15,7 +16,7 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody, user_time_format +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody, user_time_format, sanitizeNumber # pymongo db = get_db_client() @@ -252,4 +253,31 @@ def deleteInvitationCode(request): res = inv_code_collection.delete_one({'code': code}) except: return Response('Delete Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response('Code Deleted!', status.HTTP_200_OK) \ No newline at end of file + return Response('Code Deleted!', status.HTTP_200_OK) + +# currPage: number +# itemsPerPage: number +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getQARecordsByPage(request): + try: + body = decodeJSON(request.body) + sanitizeNumber(body['page']) + sanitizeNumber(body['itemsPerPage']) + except: + return Response('Invalid Body: ', status.HTTP_400_BAD_REQUEST) + + try: + arr = [] + skip = body['page'] * body['itemsPerPage'] + for inventory in inventory_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']): + # convert ObjectId + inventory['_id'] = str(inventory['_id']) + arr.append(inventory) + # if pulled array empty return no content + if len(arr) == 0: + return Response('No Result', status.HTTP_204_NO_CONTENT) + except: + return Response('Cannot Fetch From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(arr, status.HTTP_200_OK) \ No newline at end of file From 8c7bf835b3ea5dc1e918160a43e374c43a29020d Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 15 Dec 2023 19:03:03 -0500 Subject: [PATCH 034/107] fixed previous logic, added admin stuff --- CCPDController/utils.py | 2 ++ adminController/urls.py | 4 ++-- adminController/views.py | 43 +++++++++++++++++++++++++--------- imageController/urls.py | 1 - imageController/views.py | 40 ++++++++++++++++++++----------- inventoryController/models.py | 4 +++- inventoryController/views.py | 2 ++ requirements.txt | Bin 1906 -> 1948 bytes 8 files changed, 67 insertions(+), 29 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index da15335..aaf886f 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -148,6 +148,8 @@ def sanitizeInvitationCode(code): return False return code + +# these below will raise type error instead of returning false # make sure string is type str and no $ included def sanitizeString(field): if not isinstance(field, str): diff --git a/adminController/urls.py b/adminController/urls.py index 3ff5d31..6fd61e3 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -9,9 +9,9 @@ path('deleteUserById', views.deleteUserById, name="deleteUserById"), path('updateUserById/', views.updateUserById, name="updateUserById"), path('issueInvitationCode', views.issueInvitationCode, name="issueInvitationCode"), - path('getAllInventory', views.getAllInventory, name="getAllInventory"), path('getAllUserInfo', views.getAllUserInfo, name="getAllUserInfo"), path('getAllInvitationCode', views.getAllInvitationCode, name="getAllInvitationCode"), path('deleteInvitationCode', views.deleteInvitationCode, name="deleteInvitationCode"), - path('getQARecordsByPage', views.getQARecordsByPage, name="getQARecordsByPage") + path('getQARecordsByPage', views.getQARecordsByPage, name="getQARecordsByPage"), + path('deleteQARecordsBySku/', views.deleteQARecordsBySku, name="deleteQARecordsBySku"), ] diff --git a/adminController/views.py b/adminController/views.py index 606e175..bc03314 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -21,8 +21,11 @@ # pymongo db = get_db_client() user_collection = db['User'] -inventory_collection = db['Inventory'] +qa_collection = db['Inventory'] inv_code_collection = db['Invitations'] +instock_collection = db['Instock'] +retail_collection = db['Retail'] +return_collection = db['Return'] # admin jwt token expiring time admin_expire_days = 90 @@ -192,14 +195,14 @@ def updateUserById(request, uid): return Response('Password Updated', status.HTTP_200_OK) -@api_view(['GET']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def getAllInventory(request): - inv = [] - for item in inventory_collection.find({}, {'_id': 0}): - inv.append(item) - return Response(inv, status.HTTP_200_OK) +# @api_view(['GET']) +# @authentication_classes([JWTAuthentication]) +# @permission_classes([IsAdminPermission]) +# def getAllInventory(request): +# inv = [] +# for item in qa_collection.find({}, {'_id': 0}): +# inv.append(item) +# return Response(inv, status.HTTP_200_OK) @api_view(['GET']) @authentication_classes([JWTAuthentication]) @@ -271,7 +274,7 @@ def getQARecordsByPage(request): try: arr = [] skip = body['page'] * body['itemsPerPage'] - for inventory in inventory_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']): + for inventory in qa_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']): # convert ObjectId inventory['_id'] = str(inventory['_id']) arr.append(inventory) @@ -280,4 +283,22 @@ def getQARecordsByPage(request): return Response('No Result', status.HTTP_204_NO_CONTENT) except: return Response('Cannot Fetch From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(arr, status.HTTP_200_OK) \ No newline at end of file + return Response(arr, status.HTTP_200_OK) + + +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def deleteQARecordsBySku(request, sku): + try: + sanitizeNumber(int(sku)) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + try: + res = qa_collection.delete_one({sku: sku}) + if not res: + return Response('Inventory SKU Not Found', status.HTTP_404_NOT_FOUND) + except: + return Response('Failed Deleting From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('Inventory Deleted', status.HTTP_200_OK) \ No newline at end of file diff --git a/imageController/urls.py b/imageController/urls.py index ce95f25..6ed5f63 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -5,5 +5,4 @@ urlpatterns = [ path("uploadImage//", views.uploadImage, name="uploadImage"), path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), - path("listBlobContainers", views.listBlobContainers, name="listBlobContainers") ] diff --git a/imageController/views.py b/imageController/views.py index f99519c..06c0a31 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -1,4 +1,7 @@ import os +import io +import pillow_heif +from PIL import Image from time import time, ctime from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient from azure.core.exceptions import ResourceExistsError @@ -46,20 +49,39 @@ def uploadImage(request, sku, owner): # loop the files in the request for name, value in request.FILES.items(): - # add tags of owner and time info + # azure allow tags on each blob inventory_tags = { "sku": sku, "time": str(ctime(time())), "owner": owner } + # images will be uploaded to the folder named after their sku + img = value imageName = sku + '/' + sku + '_' + name + + # process apples photo format + if 'heic' in name or 'HEIC' in name: + # convert image to jpg + heicFile = pillow_heif.read_heif(value) + byteImage = Image.frombytes ( + heicFile.mode, + heicFile.size, + heicFile.data, + "raw" + ) + buf = io.BytesIO() + byteImage.save(buf, format="JPEG") + img = buf.getvalue() + # change extension to jpg + base_name = os.path.splitext(name)[0] + imageName = sku + '/' + sku + '_' + base_name + '.' + 'jpg' + try: - res = product_image_container.upload_blob(imageName, value.file, tags=inventory_tags) + res = product_image_container.upload_blob(imageName, img, tags=inventory_tags) except ResourceExistsError: return Response(imageName + 'Already Exist!', status.HTTP_409_CONFLICT) - # construct database row object # newInventoryImage = InventoryImage( # time = str(ctime(time())), @@ -71,14 +93,4 @@ def uploadImage(request, sku, owner): # push data to MongoDB # await collection.insert_one(newInventoryImage.__dict__) - return Response(res.url, status.HTTP_200_OK) - -# list blob containers -@api_view(['POST']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def listBlobContainers(request): - - - return Response('Listing blob containers......', status.HTTP_200_OK) - \ No newline at end of file + return Response(res.url, status.HTTP_200_OK) \ No newline at end of file diff --git a/inventoryController/models.py b/inventoryController/models.py index 96a0783..dac4557 100644 --- a/inventoryController/models.py +++ b/inventoryController/models.py @@ -38,10 +38,11 @@ class InventoryItem(models.Model): shelfLocation: models.CharField(max_length=4) amount: models.IntegerField(max_length=3) owner: models.CharField(max_length=32) + ownerName: models.CharField(max_length=40) marketplace: models.CharField(max_length=10, choices=MARKETPLACE_CHOISES) # constructor input all info - def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocation, amount, owner, marketplace) -> None: + def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocation, amount, owner, ownerName, marketplace) -> None: self.time = time self.sku = sku self.itemCondition=itemCondition @@ -51,6 +52,7 @@ def __init__(self, time, sku, itemCondition, comment, link, platform, shelfLocat self.shelfLocation = shelfLocation self.amount = amount self.owner = owner + self.ownerName = ownerName self.marketplace = marketplace # return inventory sku diff --git a/inventoryController/views.py b/inventoryController/views.py index 7706f6d..e9a895c 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -127,6 +127,7 @@ def createInventory(request): shelfLocation=body['shelfLocation'], amount=body['amount'], owner=body['owner'], + ownerName=body['ownerName'], marketplace=body['marketplace'] ) # pymongo need dict or bson object @@ -175,6 +176,7 @@ def updateInventoryBySku(request, sku): shelfLocation = newInv['shelfLocation'], amount = newInv['amount'], owner = newInv['owner'], + ownerName = newInv['ownerName'], marketplace = newInv['marketplace'] ) except: diff --git a/requirements.txt b/requirements.txt index 2fcad14b1b5ef36c9a084593ca64ba16245ccb18..e02eb9cd934b0e436c47d2e69fdc1a918acaccd5 100644 GIT binary patch delta 42 wcmeywH-~>i2a9YDLk>eeLpg&kLk2@CLncESgDnsmFz7KDGMH?xX8Fzp0O+d;^Z)<= delta 12 TcmbQk|A}uy2g~M7EU%aVBWwjd From 7eb00f3c7f41bbf0dfbafddb893d6669aa0f4206 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 16 Dec 2023 19:02:17 -0500 Subject: [PATCH 035/107] added admin route --- adminController/urls.py | 1 + adminController/views.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/adminController/urls.py b/adminController/urls.py index 6fd61e3..df33df6 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -14,4 +14,5 @@ path('deleteInvitationCode', views.deleteInvitationCode, name="deleteInvitationCode"), path('getQARecordsByPage', views.getQARecordsByPage, name="getQARecordsByPage"), path('deleteQARecordsBySku/', views.deleteQARecordsBySku, name="deleteQARecordsBySku"), + path('getQARecordBySku/', views.getQARecordBySku, name="getQARecordBySku"), ] diff --git a/adminController/views.py b/adminController/views.py index bc03314..7d25a42 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -296,9 +296,27 @@ def deleteQARecordsBySku(request, sku): return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) try: - res = qa_collection.delete_one({sku: sku}) + res = qa_collection.delete_one({'sku': sku}) if not res: return Response('Inventory SKU Not Found', status.HTTP_404_NOT_FOUND) except: return Response('Failed Deleting From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response('Inventory Deleted', status.HTTP_200_OK) \ No newline at end of file + return Response('Inventory Deleted', status.HTTP_200_OK) + +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getQARecordBySku(request, sku): + sku = int(sku) + try: + sanitizeNumber(sku) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + try: + res = qa_collection.find_one({'sku': sku}, {'_id': 0}) + if not res: + return Response('Inventory SKU Not Found', status.HTTP_404_NOT_FOUND) + except: + return Response('Failed Querying Database', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(res, status.HTTP_200_OK) \ No newline at end of file From 8967059276567b326cc2a2df57b8e43d3fa824c1 Mon Sep 17 00:00:00 2001 From: CccrizzZ <30523986+CccrizzZ@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:55:29 -0500 Subject: [PATCH 036/107] Update settings.py --- CCPDController/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CCPDController/settings.py b/CCPDController/settings.py index b2560ab..371c3e1 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -82,7 +82,7 @@ # https settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True -SECURE_SSL_REDIRECT = False # http -> https +SECURE_SSL_REDIRECT = True # http -> https # cookies settings SESSION_COOKIE_HTTPONLY = True From 216e12e61340fbb104f44bf67b0e85578df264ff Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 21 Dec 2023 19:09:42 -0500 Subject: [PATCH 037/107] worked on admin controller --- CCPDController/permissions.py | 4 +- CCPDController/settings.py | 2 +- CCPDController/utils.py | 2 +- adminController/models.py | 73 +++++++++++++++++++++++- adminController/urls.py | 3 + adminController/views.py | 103 ++++++++++++++++++++++++++++------ 6 files changed, 166 insertions(+), 21 deletions(-) diff --git a/CCPDController/permissions.py b/CCPDController/permissions.py index 71c661a..d17f8a6 100644 --- a/CCPDController/permissions.py +++ b/CCPDController/permissions.py @@ -29,11 +29,13 @@ class IsAdminPermission(permissions.BasePermission): def has_permission(self, request, view): if request.auth == 'Admin' and bool(request.user['userActive']) == True : return True + elif request.auth == 'Super Admin': + return True class IsSuperAdmin(permissions.BasePermission): message = 'Permission Denied, Super Admin Only!' def has_permission(self, request, view): - if request.auth == 'SAdmin': + if request.auth == 'Super Admin': return True # user blocked by IP black list diff --git a/CCPDController/settings.py b/CCPDController/settings.py index 371c3e1..b2560ab 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -82,7 +82,7 @@ # https settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True -SECURE_SSL_REDIRECT = True # http -> https +SECURE_SSL_REDIRECT = False # http -> https # cookies settings SESSION_COOKIE_HTTPONLY = True diff --git a/CCPDController/utils.py b/CCPDController/utils.py index aaf886f..2f58ca6 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -163,7 +163,7 @@ def sanitizeNumber(num): # sanitize all field in user info body # make sure user is active and remove $ -def sanitizeBody(body): +def sanitizeUserInfoBody(body): for attr, value in body.items(): # if hit user active field set the field to bool type # if not sanitize string and remove '$' diff --git a/adminController/models.py b/adminController/models.py index ecfc35c..1a48cbe 100644 --- a/adminController/models.py +++ b/adminController/models.py @@ -11,4 +11,75 @@ def __init__(self, code, exp) -> None: # return inventory sku def __str__(self) -> str: - return str(self.code) \ No newline at end of file + return str(self.code) + + +class RetailRecord(models.Model): + MARKETPLACE_CHOISES = [ + ('Hibid', 'Hibid'), + ('Retail', 'Retail'), + ('eBay', 'eBay'), + ('Wholesale', 'Wholesale'), + ('Other', 'Other') + ] + + PAYMENTMETHOD_CHOISES = [ + ('Cash', 'Cash'), + ('E-transfer', 'E-transfer'), + ('Check', 'Check'), + ('Online', 'Online'), + ('Store Credit', 'Store Credit'), + ] + + sku: models.IntegerField(max_length=6) + time: models.CharField(max_length=20) + amount: models.IntegerField(max_length=7) + quantity: models.IntegerField(max_length=4) + marketplace: models.CharField(max_length=15, choices=MARKETPLACE_CHOISES) + paymentMethod: models.CharField(max_length=15, choices=PAYMENTMETHOD_CHOISES) + buyerName: models.CharField(max_length=30) + adminName: models.CharField(max_length=30) + adminId: models.CharField(max_length=40, blank=True) + invoiceNumber: models.CharField(max_length=30, blank=True) + + def __init__(self, sku, time, amount, quantity, marketplace, paymentMethod, buyerName, adminName, adminId) -> None: + self.sku = sku + self.time = time + self.amount = amount + self.quantity=quantity + self.marketplace = marketplace + self.paymentMethod = paymentMethod + self.buyerName = buyerName + self.adminName = adminName + self.adminId = adminId + + def __str__(self) -> str: + return str(self.sku) + +class ReturnRecord(models.Model): + PAYMENTMETHOD_CHOISES = [ + ('Cash', 'Cash'), + ('E-transfer', 'E-transfer'), + ('Check', 'Check'), + ('Online', 'Online'), + ('Store Credit', 'Store Credit'), + ] + + # plus a RetailRecord models object + returnTime: models.CharField(max_length=30) + returnQuantity: models.CharField(max_length=4) + returnAmount: models.CharField(max_length=7) + refundMethod: models.CharField(max_length=30, choices=PAYMENTMETHOD_CHOISES) + reason: models.CharField(max_length=100, blank=True) + adminName: models.CharField(max_length=30) + adminId: models.CharField(max_length=40, blank=True) + + def __init__(self, returnTime, refundMethod, reason, adminName, adminId) -> None: + self.returnTime = returnTime + self.refundMethod = refundMethod + self.reason = reason + self.adminName = adminName + self.adminId = adminId + + def __str__(self) -> str: + return str(self.returnTime) \ No newline at end of file diff --git a/adminController/urls.py b/adminController/urls.py index df33df6..919d60b 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -15,4 +15,7 @@ path('getQARecordsByPage', views.getQARecordsByPage, name="getQARecordsByPage"), path('deleteQARecordsBySku/', views.deleteQARecordsBySku, name="deleteQARecordsBySku"), path('getQARecordBySku/', views.getQARecordBySku, name="getQARecordBySku"), + path('getSalesRecordsByPage', views.getSalesRecordsByPage, name="getSalesRecordsByPage"), + path('createSalesRecord', views.createSalesRecord, name="createSalesRecord"), + path('getSalesRecordsBySku/', views.getSalesRecordsBySku, name="getSalesRecordsBySku"), ] diff --git a/adminController/views.py b/adminController/views.py index 7d25a42..2dee8fd 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -8,7 +8,7 @@ from bson.objectid import ObjectId from datetime import datetime, timedelta, date from userController.models import User -from .models import InvitationCode +from .models import InvitationCode, RetailRecord from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -16,7 +16,7 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeBody, user_time_format, sanitizeNumber +from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeUserInfoBody, user_time_format, sanitizeNumber # pymongo db = get_db_client() @@ -30,12 +30,13 @@ # admin jwt token expiring time admin_expire_days = 90 +# check admin token @csrf_protect @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def checkAdminToken(request): - # get token + # get token from cookie, token is 100% set because of permission token = request.COOKIES.get('token') # decode and return user id @@ -52,7 +53,7 @@ def checkAdminToken(request): return Response({ 'id': str(ObjectId(user['_id'])), 'name': user['name']}, status.HTTP_200_OK) return Response('Token Not Found, Please Login Again', status.HTTP_100_CONTINUE) -# login any user and issue jwt +# login admins @csrf_protect @api_view(['POST']) @permission_classes([AllowAny]) @@ -106,6 +107,7 @@ def adminLogin(request): response.set_cookie('csrftoken', get_token(request), httponly=True, expires=expire, samesite="None", secure=True) return response +# create user with custom roles @csrf_protect @api_view(['POST']) @authentication_classes([JWTAuthentication]) @@ -113,7 +115,7 @@ def adminLogin(request): def createUser(request): try: body = decodeJSON(request.body) - sanitizeBody(body) + sanitizeUserInfoBody(body) newUser = User ( name=body['name'], email=body['email'], @@ -173,7 +175,7 @@ def updateUserById(request, uid): body = decodeJSON(request.body) # loop body obeject and remove $ - sanitizeBody(body) + sanitizeUserInfoBody(body) except: return Response('Invalid User Info', status.HTTP_400_BAD_REQUEST) @@ -194,16 +196,6 @@ def updateUserById(request, uid): return Response('Update User Infomation Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response('Password Updated', status.HTTP_200_OK) - -# @api_view(['GET']) -# @authentication_classes([JWTAuthentication]) -# @permission_classes([IsAdminPermission]) -# def getAllInventory(request): -# inv = [] -# for item in qa_collection.find({}, {'_id': 0}): -# inv.append(item) -# return Response(inv, status.HTTP_200_OK) - @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) @@ -319,4 +311,81 @@ def getQARecordBySku(request, sku): return Response('Inventory SKU Not Found', status.HTTP_404_NOT_FOUND) except: return Response('Failed Querying Database', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(res, status.HTTP_200_OK) \ No newline at end of file + return Response(res, status.HTTP_200_OK) + +# page: number +# itemsPerPage: number +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getSalesRecordsByPage(request): + try: + body = decodeJSON(request.body) + currPage = sanitizeNumber(int(body['currPage'])) + itemsPerPage = sanitizeNumber(int(body['itemsPerPage'])) + except: + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) + + arr = [] + skip = currPage * itemsPerPage + for record in retail_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']): + # convert ObjectId + record['_id'] = str(record['_id']) + arr.append(record) + + # if pulled array empty return no content + if len(arr) == 0: + return Response('No Result', status.HTTP_204_NO_CONTENT) + return Response(arr, status.HTTP_200_OK) + + +# RetailRecord: RetailRecord +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def createSalesRecord(request): + try: + body = decodeJSON(request.body) + newRecord = RetailRecord ( + sku=body['sku'], + time=body['time'], + amount=body['amount'], + quantity=body['quantity'], + marketplace=body['marketplace'], + paymentMethod=body['paymentMethod'], + buyerName=body['buyerName'], + adminName=body['adminName'], + # is this redundent + invoiceNumber=body['invoiceNumber'] if body['invoiceNumber'] else '', + adminId=body['adminId'] if body['adminId'] else '', + ) + except: + return Response('Invalid Body', status.HTTP_400_BAD_REQUEST) + + res = retail_collection.insert_one(newRecord) + if not res: + return Response('Cannot Insert Into DB', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('Sales Record Created', status.HTTP_200_OK) + +# one SKU could have multiple retail records with different info +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getSalesRecordsBySku(request, sku): + try: + sku = sanitizeNumber(int(sku)) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + # get all sales records associated with this sku + arr = [] + for inventory in retail_collection.find({'sku': sku}): + # convert ObjectId to string prevent error + inventory['_id'] = str(inventory['_id']) + arr.append(inventory) + if len(arr) < 1: + return Response('No Records Found', status.HTTP_404_NOT_FOUND) + return Response(arr, status.HTTP_200_OK) + + + \ No newline at end of file From 0e22f668c963d5f66efc1f1ea0aa9fba3f2d1909 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 22 Dec 2023 18:59:44 -0500 Subject: [PATCH 038/107] update admin controller --- adminController/urls.py | 3 +++ adminController/views.py | 56 +++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/adminController/urls.py b/adminController/urls.py index 919d60b..6775385 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -18,4 +18,7 @@ path('getSalesRecordsByPage', views.getSalesRecordsByPage, name="getSalesRecordsByPage"), path('createSalesRecord', views.createSalesRecord, name="createSalesRecord"), path('getSalesRecordsBySku/', views.getSalesRecordsBySku, name="getSalesRecordsBySku"), + path('createReturnRecord', views.createReturnRecord, name="createReturnRecord"), + path('getProblematicRecordsByPage', views.getProblematicRecordsByPage, name="getProblematicRecordsByPage"), + path('setProblematicBySku/', views.setProblematicBySku, name="setProblematicBySku"), ] diff --git a/adminController/views.py b/adminController/views.py index 2dee8fd..8226a9c 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -179,11 +179,6 @@ def updateUserById(request, uid): except: return Response('Invalid User Info', status.HTTP_400_BAD_REQUEST) - print('user before modification:') - print(user) - print('req body:') - print(body) - # update user information try: user_collection.update_one( @@ -388,4 +383,53 @@ def getSalesRecordsBySku(request, sku): return Response(arr, status.HTTP_200_OK) - \ No newline at end of file +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def createReturnRecord(request): + return Response('Return Record') + + +# currPage: string +# itemsPerPage: string +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getProblematicRecordsByPage(request): + try: + body = decodeJSON(request.body) + print(body['currPage']) + print(int(body['currPage'])) + print(body['itemsPerPage']) + print(int(body['itemsPerPage'])) + sanitizeNumber(int(body['currPage'])) + sanitizeNumber(int(body['itemsPerPage'])) + except: + return Response('Invalid Page Info', status.HTTP_400_BAD_REQUEST) + + arr = [] + skip = int(body['currPage']) * int(body['itemsPerPage']) + for item in qa_collection.find({ 'problem': True }).skip(skip).limit(int(body['itemsPerPage'])): + item['_id'] = str(item['_id']) + arr.append(item) + + return Response(arr, status.HTTP_200_OK) + +# set problem to true for qa records +@api_view(['PATCH']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def setProblematicBySku(request, sku): + try: + sanitizeNumber(int(sku)) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + # update record + res = qa_collection.update_one( + { 'sku': int(sku) }, + { '$set': {'problem': True} }, + ) + if not res: + return Response('Cannot Modify Records', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('Record Set', status.HTTP_200_OK) \ No newline at end of file From d4af3d9f6e6c61ca5ec8ebde69e87a638df25ab3 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 30 Dec 2023 19:01:44 -0500 Subject: [PATCH 039/107] tried to fix image controller --- adminController/urls.py | 2 +- adminController/views.py | 17 +++----------- imageController/urls.py | 2 ++ imageController/views.py | 50 +++++++++++++++++++++++++++++----------- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/adminController/urls.py b/adminController/urls.py index 6775385..4a34c54 100644 --- a/adminController/urls.py +++ b/adminController/urls.py @@ -19,6 +19,6 @@ path('createSalesRecord', views.createSalesRecord, name="createSalesRecord"), path('getSalesRecordsBySku/', views.getSalesRecordsBySku, name="getSalesRecordsBySku"), path('createReturnRecord', views.createReturnRecord, name="createReturnRecord"), - path('getProblematicRecordsByPage', views.getProblematicRecordsByPage, name="getProblematicRecordsByPage"), + path('getProblematicRecords', views.getProblematicRecords, name="getProblematicRecords"), path('setProblematicBySku/', views.setProblematicBySku, name="setProblematicBySku"), ] diff --git a/adminController/views.py b/adminController/views.py index 8226a9c..7c0bf49 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -395,27 +395,16 @@ def createReturnRecord(request): @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) -def getProblematicRecordsByPage(request): - try: - body = decodeJSON(request.body) - print(body['currPage']) - print(int(body['currPage'])) - print(body['itemsPerPage']) - print(int(body['itemsPerPage'])) - sanitizeNumber(int(body['currPage'])) - sanitizeNumber(int(body['itemsPerPage'])) - except: - return Response('Invalid Page Info', status.HTTP_400_BAD_REQUEST) - +def getProblematicRecords(request): arr = [] - skip = int(body['currPage']) * int(body['itemsPerPage']) - for item in qa_collection.find({ 'problem': True }).skip(skip).limit(int(body['itemsPerPage'])): + for item in qa_collection.find({ 'problem': True }): item['_id'] = str(item['_id']) arr.append(item) return Response(arr, status.HTTP_200_OK) # set problem to true for qa records +# isProblem: bool @api_view(['PATCH']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) diff --git a/imageController/urls.py b/imageController/urls.py index 6ed5f63..d8cfa81 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -5,4 +5,6 @@ urlpatterns = [ path("uploadImage//", views.uploadImage, name="uploadImage"), path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), + path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), + ] diff --git a/imageController/views.py b/imageController/views.py index 06c0a31..cf7af34 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -2,38 +2,67 @@ import io import pillow_heif from PIL import Image +import datetime +from datetime import timedelta from time import time, ctime -from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient +from azure.storage.blob import BlobServiceClient, BlobClient from azure.core.exceptions import ResourceExistsError from django.views.decorators.csrf import csrf_exempt from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.response import Response -from CCPDController.utils import decodeJSON, get_db_client +from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission from dotenv import load_dotenv load_dotenv() # Azure Blob +account_name = 'CCPD' +container_name = 'product-image' # blob client object from azure access keys azure_blob_client = BlobServiceClient.from_connection_string(os.getenv('SAS_KEY')) # container handle for product image -product_image_container = azure_blob_client.get_container_client("product-image") +product_image_container_client = azure_blob_client.get_container_client(container_name) # MongoDB db = get_db_client() collection = db['InventoryImage'] +images_collection = db['Images'] # download all images related to 1 sku @api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsQAPermission | IsAdminPermission]) +@permission_classes([IsAdminPermission]) def downloadAllImagesBySKU(request): print(request.data) return Response('here is all the image for sku: ', status.HTTP_200_OK) + + +# sku: str +# returns an array of image uri (for public access) +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getUrlsBySku(request): + try: + body = decodeJSON(request.body) + sanitizeNumber(int(body['sku'])) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + query = "\"sku\"='" + body['sku'] + "'" + blob_list = product_image_container_client.find_blobs_by_tags(filter_expression=query) + + arr = [] + for blob in blob_list: + blob_client = product_image_container_client.get_blob_client(blob.name) + arr.append(blob_client.url) + + return Response(arr, status.HTTP_200_OK) + # single image upload @api_view(['POST']) @authentication_classes([JWTAuthentication]) @@ -78,19 +107,12 @@ def uploadImage(request, sku, owner): imageName = sku + '/' + sku + '_' + base_name + '.' + 'jpg' try: - res = product_image_container.upload_blob(imageName, img, tags=inventory_tags) + res = product_image_container_client.upload_blob(imageName, img, tags=inventory_tags) except ResourceExistsError: return Response(imageName + 'Already Exist!', status.HTTP_409_CONFLICT) - # construct database row object - # newInventoryImage = InventoryImage( - # time = str(ctime(time())), - # sku = body["sku"], - # owner = body["owner"], - # images = body["images"] - # ) - # push data to MongoDB - # await collection.insert_one(newInventoryImage.__dict__) + # push sku with url into mongodb + images_collection.insert_one({}) return Response(res.url, status.HTTP_200_OK) \ No newline at end of file From 2dc687410f72915d20560e1a8e9d4e94cde04509 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 3 Jan 2024 19:04:01 -0500 Subject: [PATCH 040/107] fixed time and problem record route --- CCPDController/utils.py | 10 ++++++--- adminController/views.py | 6 ++++-- imageController/urls.py | 1 - inventoryController/models.py | 2 ++ inventoryController/views.py | 40 ++++++++++++++++++----------------- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 2f58ca6..4bfdb20 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -44,10 +44,14 @@ def get_client_ip(request): max_role = 12 min_role = 4 -# user format +# user registration date format user_time_format = "%b %-d %Y" -# inventory and invitation code format -time_format = "%a %b %d %H:%M:%S %Y" +# time_format = "%a %b %d %H:%M:%S %Y" + +# inventory time format +# time format should match moment.js (MMM DD YYYY HH:mm:ss) +time_format = "%b %d %Y %H:%M:%S" + # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 def convertToTime(time_str): diff --git a/adminController/views.py b/adminController/views.py index 7c0bf49..4123f2c 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -397,7 +397,7 @@ def createReturnRecord(request): @permission_classes([IsAdminPermission]) def getProblematicRecords(request): arr = [] - for item in qa_collection.find({ 'problem': True }): + for item in qa_collection.find({ 'problem': True }).sort('sku', pymongo.DESCENDING): item['_id'] = str(item['_id']) arr.append(item) @@ -410,14 +410,16 @@ def getProblematicRecords(request): @permission_classes([IsAdminPermission]) def setProblematicBySku(request, sku): try: + body = decodeJSON(request.body) sanitizeNumber(int(sku)) + isProblem = bool(body['isProblem']) except: return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) # update record res = qa_collection.update_one( { 'sku': int(sku) }, - { '$set': {'problem': True} }, + { '$set': {'problem': isProblem} }, ) if not res: return Response('Cannot Modify Records', status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/imageController/urls.py b/imageController/urls.py index d8cfa81..a141ce3 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -6,5 +6,4 @@ path("uploadImage//", views.uploadImage, name="uploadImage"), path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), - ] diff --git a/inventoryController/models.py b/inventoryController/models.py index dac4557..b6169f8 100644 --- a/inventoryController/models.py +++ b/inventoryController/models.py @@ -16,6 +16,8 @@ class InventoryItem(models.Model): PLATFORM_CHOISES = [ ('Amazon', 'Amazon'), ('eBay', 'eBay'), + ('HomeDepot', 'HomeDepot'), + ('AliExpress', 'AliExpress'), ('Official Website', 'Official Website'), ('Other', 'Other') ] diff --git a/inventoryController/views.py b/inventoryController/views.py index e9a895c..707a6f1 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -115,25 +115,27 @@ def createInventory(request): if inv: return Response('SKU Already Existed', status.HTTP_409_CONFLICT) - try: - # construct new inventory - newInventory = InventoryItem( - time=str(ctime(time())), - sku=sku, - itemCondition=body['itemCondition'], - comment=body['comment'], - link=body['link'], - platform=body['platform'], - shelfLocation=body['shelfLocation'], - amount=body['amount'], - owner=body['owner'], - ownerName=body['ownerName'], - marketplace=body['marketplace'] - ) - # pymongo need dict or bson object - res = qa_collection.insert_one(newInventory.__dict__) - except: - return Response('Invalid Inventory Information', status.HTTP_400_BAD_REQUEST) + # try: + print(body['time']) + # construct new inventory + newInventory = InventoryItem( + # time=str(ctime(time())), + time=body['time'], + sku=sku, + itemCondition=body['itemCondition'], + comment=body['comment'], + link=body['link'], + platform=body['platform'], + shelfLocation=body['shelfLocation'], + amount=body['amount'], + owner=body['owner'], + ownerName=body['ownerName'], + marketplace=body['marketplace'] + ) + # pymongo need dict or bson object + res = qa_collection.insert_one(newInventory.__dict__) + # except: + # return Response('Invalid Inventory Information', status.HTTP_400_BAD_REQUEST) return Response('Inventory Created', status.HTTP_200_OK) # query param sku and body of new inventory info From 985af51afd565860b240edb25754fb06f93f7117 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 4 Jan 2024 19:01:55 -0500 Subject: [PATCH 041/107] working on Q&A Record filters --- CCPDController/utils.py | 2 ++ adminController/views.py | 71 ++++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 4bfdb20..b68bea2 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -52,6 +52,8 @@ def get_client_ip(request): # time format should match moment.js (MMM DD YYYY HH:mm:ss) time_format = "%b %d %Y %H:%M:%S" +filter_time_format = "%Y-%m-%dT%H:%M:%S.%fZ" + # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 def convertToTime(time_str): diff --git a/adminController/views.py b/adminController/views.py index 4123f2c..3ab2c59 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -16,7 +16,17 @@ from rest_framework.exceptions import AuthenticationFailed from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication -from CCPDController.utils import decodeJSON, get_db_client, sanitizeEmail, sanitizePassword, sanitizeUserInfoBody, user_time_format, sanitizeNumber +from CCPDController.utils import ( + decodeJSON, + get_db_client, + sanitizeEmail, + sanitizePassword, + sanitizeString, + sanitizeUserInfoBody, + user_time_format, + sanitizeNumber, + filter_time_format, +) # pymongo db = get_db_client() @@ -251,23 +261,58 @@ def deleteInvitationCode(request): @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) def getQARecordsByPage(request): - try: - body = decodeJSON(request.body) - sanitizeNumber(body['page']) - sanitizeNumber(body['itemsPerPage']) - except: - return Response('Invalid Body: ', status.HTTP_400_BAD_REQUEST) - + # try: + body = decodeJSON(request.body) + sanitizeNumber(body['page']) + sanitizeNumber(body['itemsPerPage']) + + + query_filter = body['filter'] + print(query_filter) + + timeRange = query_filter['timeRangeFilter'] + print(timeRange) + + # strip the filter into mongoDB query object + fil = {} + if query_filter['conditionFilter'] != '': + sanitizeString(query_filter['conditionFilter']) + fil['itemCondition'] = query_filter['conditionFilter'] + if query_filter['platformFilter'] != '': + sanitizeString(query_filter['platformFilter']) + fil['platform'] = query_filter['platformFilter'] + if query_filter['marketplaceFilter'] != '': + sanitizeString(query_filter['marketplaceFilter']) + fil['marketplace'] = query_filter['marketplaceFilter'] + if timeRange != {}: + sanitizeString(timeRange['from']) + sanitizeString(timeRange['to']) + fil['time'] = { + '$gte': datetime.strptime(timeRange['from'], filter_time_format), + '$lt': datetime.strptime(timeRange['to'], filter_time_format) + } + print(fil) + + # except: + # return Response('Invalid Body: ', status.HTTP_400_BAD_REQUEST) + + + try: arr = [] skip = body['page'] * body['itemsPerPage'] - for inventory in qa_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']): - # convert ObjectId - inventory['_id'] = str(inventory['_id']) - arr.append(inventory) + + if fil == {}: + query = qa_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']) + else: + query = qa_collection.find(fil).sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']) + for inventory in query: + inventory['_id'] = str(inventory['_id']) + arr.append(inventory) + # if pulled array empty return no content if len(arr) == 0: - return Response('No Result', status.HTTP_204_NO_CONTENT) + return Response([], status.HTTP_204_NO_CONTENT) except: return Response('Cannot Fetch From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(arr, status.HTTP_200_OK) From 63cfac5bf4455ce29d9e5fbeba903dda5b790f02 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 10 Jan 2024 18:57:47 -0500 Subject: [PATCH 042/107] fixed image controller --- CCPDController/utils.py | 1 + adminController/views.py | 18 ++++++------ imageController/urls.py | 4 +-- imageController/views.py | 62 ++++++++++++++++++++++++++++------------ 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index b68bea2..f492f3e 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -166,6 +166,7 @@ def sanitizeString(field): def sanitizeNumber(num): if not isinstance(num, int): raise TypeError('Invalid Information') + return num # sanitize all field in user info body # make sure user is active and remove $ diff --git a/adminController/views.py b/adminController/views.py index 3ab2c59..dcf04dc 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -265,15 +265,14 @@ def getQARecordsByPage(request): body = decodeJSON(request.body) sanitizeNumber(body['page']) sanitizeNumber(body['itemsPerPage']) - - + query_filter = body['filter'] print(query_filter) timeRange = query_filter['timeRangeFilter'] print(timeRange) - # strip the filter into mongoDB query object + # strip the filter into mongoDB query object in fil fil = {} if query_filter['conditionFilter'] != '': sanitizeString(query_filter['conditionFilter']) @@ -292,30 +291,31 @@ def getQARecordsByPage(request): '$lt': datetime.strptime(timeRange['to'], filter_time_format) } print(fil) - + # except: # return Response('Invalid Body: ', status.HTTP_400_BAD_REQUEST) - - try: arr = [] skip = body['page'] * body['itemsPerPage'] - + if fil == {}: query = qa_collection.find().sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']) + count = qa_collection.count_documents({}) else: query = qa_collection.find(fil).sort('sku', pymongo.DESCENDING).skip(skip).limit(body['itemsPerPage']) + count = qa_collection.count_documents(fil) + for inventory in query: inventory['_id'] = str(inventory['_id']) arr.append(inventory) # if pulled array empty return no content if len(arr) == 0: - return Response([], status.HTTP_204_NO_CONTENT) + return Response([], status.HTTP_200_OK) except: return Response('Cannot Fetch From Database', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(arr, status.HTTP_200_OK) + return Response({"arr": arr, "count": count}, status.HTTP_200_OK) @api_view(['POST']) diff --git a/imageController/urls.py b/imageController/urls.py index a141ce3..36fea66 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -3,7 +3,7 @@ # define all routes urlpatterns = [ - path("uploadImage//", views.uploadImage, name="uploadImage"), - path("downloadAllImagesBySKU", views.downloadAllImagesBySKU, name="downloadAllImagesBySKU"), + path("uploadImage///", views.uploadImage, name="uploadImage"), + path("getUrlsByOwner", views.getUrlsByOwner, name="getUrlsByOwner"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), ] diff --git a/imageController/views.py b/imageController/views.py index cf7af34..35de52e 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -11,7 +11,7 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.response import Response -from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber +from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeString from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission from dotenv import load_dotenv @@ -25,21 +25,31 @@ # container handle for product image product_image_container_client = azure_blob_client.get_container_client(container_name) -# MongoDB -db = get_db_client() -collection = db['InventoryImage'] -images_collection = db['Images'] +# # MongoDB +# db = get_db_client() -# download all images related to 1 sku +# return array of all image url from owner @api_view(['POST']) @authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def downloadAllImagesBySKU(request): +@permission_classes([IsQAPermission | IsAdminPermission]) +def getUrlsByOwner(request): + try: + body = decodeJSON(request.body) + sanitizeString(body['owner']) + except: + return Response('Invalid Owner Id', status.HTTP_400_BAD_REQUEST) + + query = "\"owner\"='" + body['owner'] + "'" + blob_list = product_image_container_client.find_blobs_by_tags(filter_expression=query) - print(request.data) - return Response('here is all the image for sku: ', status.HTTP_200_OK) - + arr = [] + for blob in blob_list: + blob_client = product_image_container_client.get_blob_client(blob.name) + arr.append(blob_client.url) + if len(arr) < 1: + return Response('No Images Found for Owner', status.HTTP_404_NOT_FOUND) + return Response(arr, status.HTTP_200_OK) # sku: str # returns an array of image uri (for public access) @@ -60,15 +70,17 @@ def getUrlsBySku(request): for blob in blob_list: blob_client = product_image_container_client.get_blob_client(blob.name) arr.append(blob_client.url) - + + if len(arr) < 1: + return Response('No images found for sku', status.HTTP_404_NOT_FOUND) return Response(arr, status.HTTP_200_OK) # single image upload @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsQAPermission | IsAdminPermission]) -def uploadImage(request, sku, owner): - # request body is unreadable binary code +def uploadImage(request, ownerId, owner, sku): + # request body content type is file form therefore only binary data allowed # sku will be in the path parameter # request.FILES looks like this and is a multi-value dictionary # { @@ -82,7 +94,8 @@ def uploadImage(request, sku, owner): inventory_tags = { "sku": sku, "time": str(ctime(time())), - "owner": owner + "owner": ownerId, + "ownerName": owner } # images will be uploaded to the folder named after their sku @@ -110,9 +123,20 @@ def uploadImage(request, sku, owner): res = product_image_container_client.upload_blob(imageName, img, tags=inventory_tags) except ResourceExistsError: return Response(imageName + 'Already Exist!', status.HTTP_409_CONFLICT) + return Response(res.url, status.HTTP_200_OK) + +@api_view(['DELETE']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsQAPermission | IsAdminPermission]) +def deleteImageByName(request): + body = decodeJSON(request.body) + sku = body['sku'] + name = body['name'] + imageName = sku + '/' + sku + '_' + name + print(imageName) + # res = product_image_container_client.delete_blob(imageName) + # print(res) - # push sku with url into mongodb - images_collection.insert_one({}) - - return Response(res.url, status.HTTP_200_OK) \ No newline at end of file + + return Response('Image Deleted', status.HTTP_200_OK) \ No newline at end of file From 5710c482a308c02cbe63893e87563256f2803565 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Thu, 11 Jan 2024 18:19:32 -0500 Subject: [PATCH 043/107] fixed image controller - tags object took out of for loop so every item can have tags - Azure automatically unquote urls so delete name should be unquoted --- imageController/urls.py | 1 + imageController/views.py | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/imageController/urls.py b/imageController/urls.py index 36fea66..bf610f8 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -6,4 +6,5 @@ path("uploadImage///", views.uploadImage, name="uploadImage"), path("getUrlsByOwner", views.getUrlsByOwner, name="getUrlsByOwner"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), + path("deleteImageByName", views.deleteImageByName, name="deleteImageByName") ] diff --git a/imageController/views.py b/imageController/views.py index 35de52e..ab8d65c 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -15,6 +15,7 @@ from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission from dotenv import load_dotenv +from urllib import parse load_dotenv() # Azure Blob @@ -25,9 +26,6 @@ # container handle for product image product_image_container_client = azure_blob_client.get_container_client(container_name) -# # MongoDB -# db = get_db_client() - # return array of all image url from owner @api_view(['POST']) @authentication_classes([JWTAuthentication]) @@ -88,15 +86,16 @@ def uploadImage(request, ownerId, owner, sku): # 'IMG_20231110_150000.jpg': [] # } + # azure allow tags on each blob + inventory_tags = { + "sku": sku, + "time": str(ctime(time())), + "owner": ownerId, + "ownerName": owner + } + # loop the files in the request for name, value in request.FILES.items(): - # azure allow tags on each blob - inventory_tags = { - "sku": sku, - "time": str(ctime(time())), - "owner": ownerId, - "ownerName": owner - } # images will be uploaded to the folder named after their sku img = value @@ -133,10 +132,10 @@ def deleteImageByName(request): sku = body['sku'] name = body['name'] - imageName = sku + '/' + sku + '_' + name - print(imageName) - # res = product_image_container_client.delete_blob(imageName) - # print(res) - - + # azure automatically unquote all % in url + imageName = parse.unquote(sku + '/' + name) + try: + res = product_image_container_client.delete_blob(imageName) + except: + return Response('No Such Image', status.HTTP_404_NOT_FOUND) return Response('Image Deleted', status.HTTP_200_OK) \ No newline at end of file From 00f53d7402a6d6a695ac60d75a20679737b1e73b Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Sat, 13 Jan 2024 19:01:35 -0500 Subject: [PATCH 044/107] tried to fix time range problem fixed blob tag problem --- CCPDController/utils.py | 24 +++++++++++++++++++----- adminController/views.py | 16 ++++++++++++---- imageController/urls.py | 2 +- imageController/views.py | 21 ++++++++++++--------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index f492f3e..4e894d9 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,7 +1,8 @@ -from datetime import datetime +from datetime import datetime, timedelta import os import json from django.conf import settings +import pytz from rest_framework.response import Response from rest_framework import exceptions from pymongo import MongoClient @@ -46,14 +47,27 @@ def get_client_ip(request): # user registration date format user_time_format = "%b %-d %Y" -# time_format = "%a %b %d %H:%M:%S %Y" + +# Q&A table time filter format +filter_time_format = "%Y-%m-%dT%H:%M:%S.%fZ" + +# image blob date format +blob_date_format = "%a %b %d %Y" + +# return blob time string with format of blob date format +def getBlobTimeString() -> str: + eastern_timezone = pytz.timezone('America/Toronto') + current_time = datetime.now(eastern_timezone) + return current_time.strftime(blob_date_format) + +def getNDayBefore(days_before, time_str) -> str: + blob_time = datetime.strptime(time_str, blob_date_format) + blob_time = blob_time - timedelta(days=days_before) + return blob_time.strftime(blob_date_format) # inventory time format # time format should match moment.js (MMM DD YYYY HH:mm:ss) time_format = "%b %d %Y %H:%M:%S" - -filter_time_format = "%Y-%m-%dT%H:%M:%S.%fZ" - # convert from string to datetime # example: Thu Oct 12 18:48:49 2023 def convertToTime(time_str): diff --git a/adminController/views.py b/adminController/views.py index dcf04dc..61adbca 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -271,8 +271,8 @@ def getQARecordsByPage(request): timeRange = query_filter['timeRangeFilter'] print(timeRange) - - # strip the filter into mongoDB query object in fil + + # strip the ilter into mongoDB query object in fil fil = {} if query_filter['conditionFilter'] != '': sanitizeString(query_filter['conditionFilter']) @@ -286,9 +286,17 @@ def getQARecordsByPage(request): if timeRange != {}: sanitizeString(timeRange['from']) sanitizeString(timeRange['to']) + gt = datetime.fromisoformat(timeRange['from'][:-1]) + lt = datetime.fromisoformat(timeRange['to'][:-1]) fil['time'] = { - '$gte': datetime.strptime(timeRange['from'], filter_time_format), - '$lt': datetime.strptime(timeRange['to'], filter_time_format) + # '$gte': datetime.strptime(timeRange['from'], time_format), + # '$lt': datetime.strptime(timeRange['to'], time_format) + # '$gte': gt.strftime("%a %b %-d %H:%M:%S %Y"), + # '$lt': lt.strftime("%a %b %-d %H:%M:%S %Y") + + # mongoDB time range query only support format like '2024-01-03T05:00:00.000Z' + '$gte': timeRange['from'], + '$lt': timeRange['to'] } print(fil) diff --git a/imageController/urls.py b/imageController/urls.py index bf610f8..0931e75 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -6,5 +6,5 @@ path("uploadImage///", views.uploadImage, name="uploadImage"), path("getUrlsByOwner", views.getUrlsByOwner, name="getUrlsByOwner"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), - path("deleteImageByName", views.deleteImageByName, name="deleteImageByName") + path("deleteImageByName", views.deleteImageByName, name="deleteImageByName"), ] diff --git a/imageController/views.py b/imageController/views.py index ab8d65c..3931823 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -11,7 +11,7 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.response import Response -from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeString +from CCPDController.utils import decodeJSON, getNDayBefore, sanitizeNumber, sanitizeString, getBlobTimeString from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission from dotenv import load_dotenv @@ -36,8 +36,13 @@ def getUrlsByOwner(request): sanitizeString(body['owner']) except: return Response('Invalid Owner Id', status.HTTP_400_BAD_REQUEST) - - query = "\"owner\"='" + body['owner'] + "'" + + # format: Wed Jan 10 2024 + # filter image created within 2 days + time = "\"time\"<='" + getNDayBefore(2, getBlobTimeString()) + "'" + owner = "\"owner\"='" + body['owner'] + "'" + query = owner + " AND " + time + print(query) blob_list = product_image_container_client.find_blobs_by_tags(filter_expression=query) arr = [] @@ -60,9 +65,8 @@ def getUrlsBySku(request): sanitizeNumber(int(body['sku'])) except: return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) - - query = "\"sku\"='" + body['sku'] + "'" - blob_list = product_image_container_client.find_blobs_by_tags(filter_expression=query) + sku = "sku = '" + body['sku'] + "'" + blob_list = product_image_container_client.find_blobs_by_tags(filter_expression=sku) arr = [] for blob in blob_list: @@ -89,14 +93,13 @@ def uploadImage(request, ownerId, owner, sku): # azure allow tags on each blob inventory_tags = { "sku": sku, - "time": str(ctime(time())), + "time": getBlobTimeString(), # format: Wed Jan 10 2024 "owner": ownerId, "ownerName": owner } # loop the files in the request for name, value in request.FILES.items(): - # images will be uploaded to the folder named after their sku img = value imageName = sku + '/' + sku + '_' + name @@ -116,7 +119,7 @@ def uploadImage(request, ownerId, owner, sku): img = buf.getvalue() # change extension to jpg base_name = os.path.splitext(name)[0] - imageName = sku + '/' + sku + '_' + base_name + '.' + 'jpg' + imageName = sku + '/' + base_name + '.' + 'jpg' try: res = product_image_container_client.upload_blob(imageName, img, tags=inventory_tags) From d4d4e8ae4003efefe452d52abaf970e2196a76ec Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 16 Jan 2024 19:01:32 -0500 Subject: [PATCH 045/107] added chat gpt api integration --- CCPDController/amz_scrape_utils.py | 0 CCPDController/chat_gpt_utils.py | 171 +++++++++++++++++++++++++++++ CCPDController/utils.py | 2 +- adminController/views.py | 7 +- imageController/urls.py | 1 + imageController/views.py | 33 +++++- inventoryController/urls.py | 4 +- inventoryController/views.py | 32 +++++- requirements.txt | Bin 1948 -> 2348 bytes 9 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 CCPDController/amz_scrape_utils.py create mode 100644 CCPDController/chat_gpt_utils.py diff --git a/CCPDController/amz_scrape_utils.py b/CCPDController/amz_scrape_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/CCPDController/chat_gpt_utils.py b/CCPDController/chat_gpt_utils.py new file mode 100644 index 0000000..e9f12fb --- /dev/null +++ b/CCPDController/chat_gpt_utils.py @@ -0,0 +1,171 @@ +# send +import os +from openai import OpenAI +from dotenv import load_dotenv +load_dotenv() + +# chat gpt's natural language processing model engine's name & key +model_engine = "gpt-3.5-turbo-instruct" +# openai.api_key = os.getenv('OPENAI_API_KEY') +client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) + + +# TypeError: Missing required arguments; Expected either ('messages' and 'model') or ('messages', 'model' and 'stream') arguments to be given + + + + +# short description (lead on auction flex max 40 char ) +def generate_short_product_title(description): + prompt = f"You are an Auctioneer, based on the information, create a short product title. The character limit for product title is 30 byte. {description}." + res = client.chat.completions.create( + # model_engine=model_engine, + prompt=prompt, + max_tokens=40, + n=1, + stop=None, + temperature=0.5, + model=model_engine + ) + title = res.choices[0].text.strip() + return title + +# full description +def generate_full_product_title(comment, description): + comment = special_characters_convert_c(comment) + prompt = ( + f"Additional Condition: {comment}." + f"Item information: {description}." + f"Please generate a product title in the format [Additional Condition] - [Item information], The character limit for Item information is 150 byte." + ) + res = client.chat.completions.create( + model=model_engine, + prompt=prompt, + max_tokens=100, + n=1, + stop=None, + temperature=0.4, + ) + print(res) + Newdescription = res.choices[0].text.strip() + return Newdescription + +# convert initial to full word +def special_characters_convert_c(comment): + comment = comment + try: + comment = comment.replace("UT.","UNTEST ") + comment = comment.replace("MP.","MISSING PARTS ") + comment = comment.replace("FT.","FUNCTION TEST ") + comment = comment.replace("SI.","IMAGE SHOW SIMILAR ITEM ") + comment = comment.replace("PT.","POWER TEST ") + comment = comment.replace("API.","ALL PARTS IN ") + comment = comment.replace("MA.","MISSING ACCESSORIES ") + return comment + except: + raise TypeError("Only string comments are allowed") + +def special_characters_convert_d(description): + try: + if description[0] == '"' and description[-1] == '"': + description = description.strip() + description = description[1:-1] + except Exception as e: + print(e) + description = description.replace("°"," Degree ") + description = description.replace("™","") + description = description.replace("®","") + description = description.replace("©","") + description = description.replace("w/ ","With ") + description = description.replace("w/","With ") + description = description.replace("W/ ","With ") + description = description.replace("W/","With ") + description = description.replace(",",",") + description = description.replace("“",'"') + description = description.replace("”",'"') + description = description.replace("’’",'"') + description = description.replace("’","'") + description = description.replace("‘","'") + description = description.replace("″",'"') + description = description.replace("【","[") + description = description.replace("】","]") + description = description.replace("×","x") + description = description.replace("–","-") + description = description.replace("℉"," Fahrenheit scale ") + description = description.replace(" "," ") + description = description.replace("Φ"," ") + description = description.replace("’","'") + description = description.replace('Additional Condition" - "',"") + description = description.replace("Additional Condition - ","") + description = description.replace("Additional Condition: ","") + description = description.replace("[Additional Condition] - ","") + description = description.replace("Item Info: ","") + description = description.replace("é","e") + description = description.replace("(","(") + description = description.replace(")",")") + description = description.replace("≤"," less than or equal than ") + description = description.replace("ӧ","o") + description = description.replace("ö","o") + description = description.replace("î","i") + description = description.replace('"Additional Condition" - ',"") + description = description.replace("150 Byte: ","") + description = description.replace("150 byte: ","") + description = description.replace("150 Byte - ","") + description = description.replace("150 byte - ","") + description = description.replace("[Additional Condition] ","") + description = description.replace("ñ","n") + description = description.replace("New - ","") + description = description.replace("[Item information]","") + description = description.replace("Item information: ","") + description = description.replace("²","^2") + description = description.replace("³","^3") + description = description.replace("à","a") + return description + +def special_characters_convert_l(lead): + try: + if lead[0] == '"' and lead[-1] == '"': + lead = lead.strip() + lead = lead[1:-1] + if "(" in lead: + index1 = lead.find("(") + index2 = lead.find(")") + leadH = lead[:index1] + leadB = lead[index2+1:] + lead = leadH + leadB + if "," in lead: + index3 = lead.find(",") + lead = lead[:index3] + except Exception as e: + print(e) + lead = lead.replace("°","Degree") + lead = lead.replace("™","") + lead = lead.replace("®","") + lead = lead.replace("©","") + lead = lead.replace("w/ ","With ") + lead = lead.replace("w/","With ") + lead = lead.replace(",",",") + lead = lead.replace("“",'"') + lead = lead.replace("”",'"') + lead = lead.replace("″",'"') + lead = lead.replace("’’",'"') + lead = lead.replace("’","'") + lead = lead.replace("‘","'") + lead = lead.replace("【","[") + lead = lead.replace("】","]") + lead = lead.replace("×","x") + lead = lead.replace("–","-") + lead = lead.replace("℉"," Fahrenheit scale") + lead = lead.replace(" "," ") + lead = lead.replace("Φ"," ") + lead = lead.replace("î","i") + lead = lead.replace("é","e") + lead = lead.replace("(","(") + lead = lead.replace(")",")") + lead = lead.replace("≤"," less than or equal than ") + lead = lead.replace("ӧ","o") + lead = lead.replace("ö","o") + lead = lead.replace("ñ","n") + lead = lead.replace("²"," Squared") + lead = lead.replace("à","a") + return lead diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 4e894d9..12a7e0c 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -13,7 +13,7 @@ # ssl hand shake error because ip not whitelisted client = MongoClient( os.getenv('DATABASE_URL'), - maxPoolSize=1 + maxPoolSize=2 ) db_handle = client[os.getenv('DB_NAME')] def get_db_client(): diff --git a/adminController/views.py b/adminController/views.py index 61adbca..05a5d52 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -289,12 +289,7 @@ def getQARecordsByPage(request): gt = datetime.fromisoformat(timeRange['from'][:-1]) lt = datetime.fromisoformat(timeRange['to'][:-1]) fil['time'] = { - # '$gte': datetime.strptime(timeRange['from'], time_format), - # '$lt': datetime.strptime(timeRange['to'], time_format) - # '$gte': gt.strftime("%a %b %-d %H:%M:%S %Y"), - # '$lt': lt.strftime("%a %b %-d %H:%M:%S %Y") - - # mongoDB time range query only support format like '2024-01-03T05:00:00.000Z' + # mongoDB time range query only support ISO 8601 format like '2024-01-03T05:00:00.000Z' '$gte': timeRange['from'], '$lt': timeRange['to'] } diff --git a/imageController/urls.py b/imageController/urls.py index 0931e75..92a5567 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -7,4 +7,5 @@ path("getUrlsByOwner", views.getUrlsByOwner, name="getUrlsByOwner"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), path("deleteImageByName", views.deleteImageByName, name="deleteImageByName"), + path("scrapeStockImageBySku", views.scrapeStockImageBySku, name="scrapeStockImageBySku"), ] diff --git a/imageController/views.py b/imageController/views.py index 3931823..47452c6 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -5,19 +5,23 @@ import datetime from datetime import timedelta from time import time, ctime -from azure.storage.blob import BlobServiceClient, BlobClient +from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceExistsError -from django.views.decorators.csrf import csrf_exempt from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.response import Response -from CCPDController.utils import decodeJSON, getNDayBefore, sanitizeNumber, sanitizeString, getBlobTimeString +from CCPDController.utils import decodeJSON, getNDayBefore, sanitizeNumber, sanitizeString, getBlobTimeString, get_db_client from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission from dotenv import load_dotenv from urllib import parse +import pymongo load_dotenv() +# Mongo DB +db = get_db_client() +qa_collection = db['Inventory'] + # Azure Blob account_name = 'CCPD' container_name = 'product-image' @@ -141,4 +145,25 @@ def deleteImageByName(request): res = product_image_container_client.delete_blob(imageName) except: return Response('No Such Image', status.HTTP_404_NOT_FOUND) - return Response('Image Deleted', status.HTTP_200_OK) \ No newline at end of file + return Response('Image Deleted', status.HTTP_200_OK) + + +# pull one stock image from website and save it to blob container under that sku +# sku: str +@api_view(['POST']) +def scrapeStockImageBySku(request): + body = decodeJSON(request.body) + sku = sanitizeNumber(int(body['sku'])) + print(sku) + + # find target inventory + target = qa_collection.find_one({ 'sku': sku }) + if not target: + return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) + + # pull link from inventory db + print(target['link']) + # look up that link and save it to blob container on Azure + + + return Response(target['link'], status.HTTP_200_OK) \ No newline at end of file diff --git a/inventoryController/urls.py b/inventoryController/urls.py index c800b02..c8c242a 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -8,5 +8,7 @@ path("deleteInventoryBySku", views.deleteInventoryBySku, name="deleteInventoryBySku"), path("updateInventoryBySku/", views.updateInventoryBySku, name="updateInventoryBySku"), path("getInventoryByOwnerId/", views.getInventoryByOwnerId, name="getInventoryByOwnerId"), - path("getInventoryInfoByOwnerId", views.getInventoryInfoByOwnerId, name="getInventoryInfoByOwnerId") + path("getInventoryInfoByOwnerId", views.getInventoryInfoByOwnerId, name="getInventoryInfoByOwnerId"), + path("getAllShelfLocations", views.getAllShelfLocations, name="getAllShelfLocations"), + path("generateDescriptionBySku", views.generateDescriptionBySku, name="generateDescriptionBySku") ] diff --git a/inventoryController/views.py b/inventoryController/views.py index 707a6f1..1a5030e 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -11,6 +11,7 @@ from rest_framework import status from bson.objectid import ObjectId from collections import Counter +from CCPDController.chat_gpt_utils import generate_short_product_title, generate_full_product_title import pymongo # pymongo @@ -243,4 +244,33 @@ def deleteInventoryBySku(request): if canDel: de = qa_collection.delete_one({'sku': sku}) return Response('Inventory Deleted', status.HTTP_200_OK) - return Response('Cannot Delete Inventory After 24H, Please Contact Admin', status.HTTP_403_FORBIDDEN) \ No newline at end of file + return Response('Cannot Delete Inventory After 24H, Please Contact Admin', status.HTTP_403_FORBIDDEN) + +# get all shelf location for existing inventories +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def getAllShelfLocations(): + arr = qa_collection.distinct('shelfLocation') + return Response(arr, status.HTTP_200_OK) + +# description: string +@api_view(['GET']) +def generateDescriptionBySku(request): + try: + body = decodeJSON(request.body) + sku = sanitizeSku(body['sku']) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + print(sku) + # grab comment from that specific item + comment = qa_collection.find_one({'sku': sku}, {'comment': 1}) + if not comment: + return Response('Inventory Not Found', status.HTTP_404_NOT_FOUND) + + # call chat gpt to generate description + lead = generate_short_product_title('USED LIKE NEW - MISISNG 2 ACCESSORIES - Double Canister Dry Food Dispenser Convenient Storage') + # full_lead = generate_full_product_title(comment['comment'], '') + + return Response(lead, status.HTTP_200_OK) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e02eb9cd934b0e436c47d2e69fdc1a918acaccd5..62b597f52209b8468784c4070401329138411982 100644 GIT binary patch delta 400 zcmZ8d%?bfw6ulp^P*Tc5@(AP`3>NYLO3BJXnha6Q_?ZO{;0b2y5o|O|58@##yaCR= z%oN>j^EKz3d(OS5%sJb8JuPxjE_r03r({zJ-vh1j-ccJ>p)#513Z*FpmIHJF1!Fb* zHzMp3GgZN*CBQ9+D|9%3iKegBFmFS=gT?kcLIY{>h~TKMsy`=<3T0XxKF)K9Jmi|l z_^EJM%=rw-7Bcpc-hgQ%e$TtIkApyqNY`YoUrgM(Yiwl8W*+c~FnFsvx+;_Z8RMT6 n%>5)agGXYZat_AKP~n+4X4P*bOSuhlCI!;7NCr*;lePyUv-Afz0bpkj Ag#Z8m From 169a7fdfad2f1b321ff1be29f0d495c7a8a43c5d Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 17 Jan 2024 19:00:01 -0500 Subject: [PATCH 046/107] working on scrape functions --- CCPDController/amz_scrape_utils.py | 0 CCPDController/scrape_utils.py | 161 +++++++++++++++++++++++++++++ CCPDController/settings.py | 2 +- imageController/views.py | 37 +++++-- inventoryController/urls.py | 4 +- inventoryController/views.py | 105 ++++++++++++++++++- requirements.txt | Bin 2348 -> 3914 bytes 7 files changed, 293 insertions(+), 16 deletions(-) delete mode 100644 CCPDController/amz_scrape_utils.py create mode 100644 CCPDController/scrape_utils.py diff --git a/CCPDController/amz_scrape_utils.py b/CCPDController/amz_scrape_utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/CCPDController/scrape_utils.py b/CCPDController/scrape_utils.py new file mode 100644 index 0000000..534c42d --- /dev/null +++ b/CCPDController/scrape_utils.py @@ -0,0 +1,161 @@ +# ''' +# Scrape stock images URL from Amazon, HomeDepot etc. +# Original Author: ouyangxue-0407 +# ''' +# import time +# import random +# import requests +# from PIL import Image +# from io import BytesIO +from fake_useragent import UserAgent +# from selenium import webdriver +# from selenium.webdriver.common.by import By +# from selenium.webdriver.chrome.options import Options + +# # random wait time between scrape +# random_wait = lambda : time.sleep(random.randint(30, 50)) + +# # # Amazon US +# # def search_amazon(self: str): +# # url = f"https://www.amazon.com/s?k={self.query}" +# # page = self.context.new_page() +# # page.goto(url) +# # page.wait_for_load_state('domcontentloaded') +# # first_product_link = page.query_selector("a.a-link-normal") +# # if first_product_link: +# # first_product_img = first_product_link.query_selector("img") +# # if first_product_img: +# # img_src = first_product_img.get_attribute('src') +# # return img_src +# # return None + +# # # Amazon CA +# # def search_amazon_ca(query: str): +# # play = sync_playwright().start() +# # browser = play.chromium.launch( +# # headless=True, +# # channel="msedge", +# # args=[ +# # '--enable-automation', +# # '--window-size=1280,768', +# # ], +# # ) +# # context = browser.new_context() +# # url = f"https://www.amazon.ca/s?k={query}" +# # page = context.new_page() +# # page.goto(url) +# # page.wait_for_load_state('domcontentloaded') + +# # # start scraping +# # first_product_link = page.query_selector("a.a-link-normal") +# # if not first_product_link: +# # return None + +# # first_product_img = first_product_link.query_selector("img") +# # if not first_product_img: +# # return None + +# # img_src = first_product_img.get_attribute('src') +# # browser.close() +# # return img_src + +# # # HomeDepot US +# # def search_homedepot(query: str): +# # url = f"https://www.homedepot.com/s/{query}" +# # page = self.context.new_page() +# # page.goto(url) +# # page.wait_for_load_state('domcontentloaded') +# # first_product_link = page.query_selector("a.product-image") +# # if first_product_link: +# # first_product_img = first_product_link.query_selector("img") +# # if first_product_img: +# # img_src = first_product_img.get_attribute('src') +# # return img_src +# # return None + +# # # HomeDepot CA +# # def search_homedepot_ca(query: str): +# # url = f"https://www.homedepot.ca/search?q={query}" +# # page = self.context.new_page() +# # page.goto(url) +# # page.wait_for_load_state('domcontentloaded') +# # first_product_link = page.query_selector("a.acl-product-card__image-link") +# # if first_product_link: +# # first_product_img = first_product_link.query_selector("img") +# # if first_product_img: +# # img_src = first_product_img.get_attribute('src') +# # return img_src +# # return None + +# generate 10 Random user agent +def generate_ua(): + ua = UserAgent() + user_agents = [] + for i in range(10): + user_agent = ua.random + user_agents.append(user_agent) + return user_agents + +# # user agent array +# uaArr = generate_ua() + +# # selenium webdriver options +# options = webdriver.ChromeOptions() +# options.add_argument("--disable-blink-features=AutomationControlled") +# options.add_argument("--disable-extensions") +# options.add_argument("--profile-directory=Default") +# options.add_argument("--disable-plugins-discovery") +# options.add_argument("--lang=en") +# options.add_argument(f'user-agent={random.choice(uaArr)}') + + + +# def create_driver(user_agent): +# options = Options() +# options = webdriver.ChromeOptions() +# options.add_argument("--disable-blink-features=AutomationControlled") +# options.add_argument("--disable-extensions") +# options.add_argument("--profile-directory=Default") +# options.add_argument("--disable-plugins-discovery") +# options.add_argument("--lang=en") +# options.add_argument(f'user-agent={random.choice(user_agent)}') +# options.binary_location = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" +# options.add_experimental_option('excludeSwitches', ['enable-logging']) +# driver = webdriver.Chrome(options=options, executable_path='d:\\CCP-AUTOMATION\\chromedriver.exe') +# return driver + +# def scrape(url: str): +# driver = webdriver.Chrome(options=options) +# driver.get(url) +# div_elements = driver.find_elements(By.CSS_SELECTOR, "div.mediagallery__mainimage") +# if div_elements: +# img_element = div_elements[0].find_element("tag name", "img") +# img_url = img_element.get_attribute("src") +# response = requests.get(img_url) +# img_bytes = BytesIO(response.content) +# img = Image.open(img_bytes) +# img.save(os.path.join(target_folder, f"{item}.jpg")) +# return driver +# #except Except: +# else: +# div_elements = driver.find_elements(By.CSS_SELECTOR, "div.canvas-container") +# if div_elements: +# button=div_elements[0].find_element(By.CLASS_NAME, "modal-button") +# button.click() +# div_img_elements = div_elements[0].find_elements(By.XPATH, "//div[contains(@class, 'image-container') and contains(@class, 'ng-star-inserted')]") +# img_element = div_img_elements[0].find_element("tag name", "img") +# img_url = img_element.get_attribute("src") +# try: +# with urllib.request.urlopen(url, timeout=5) as response: +# image_content = response.read() +# with open(os.path.join(target_folder, f"{item}.jpg"), "wb") as file: +# file.write(image_content) +# return driver +# except: +# print("No Image Found Time Out") +# return driver +# else: +# print("No Image Found") +# return driver +# return None + diff --git a/CCPDController/settings.py b/CCPDController/settings.py index b2560ab..87d30a0 100644 --- a/CCPDController/settings.py +++ b/CCPDController/settings.py @@ -182,4 +182,4 @@ # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/imageController/views.py b/imageController/views.py index 47452c6..2694709 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -1,5 +1,7 @@ import os import io +import random +import requests import pillow_heif from PIL import Image import datetime @@ -13,6 +15,7 @@ from CCPDController.utils import decodeJSON, getNDayBefore, sanitizeNumber, sanitizeString, getBlobTimeString, get_db_client from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission +from CCPDController.scrape_utils import generate_ua from dotenv import load_dotenv from urllib import parse import pymongo @@ -147,23 +150,37 @@ def deleteImageByName(request): return Response('No Such Image', status.HTTP_404_NOT_FOUND) return Response('Image Deleted', status.HTTP_200_OK) - # pull one stock image from website and save it to blob container under that sku # sku: str @api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) def scrapeStockImageBySku(request): body = decodeJSON(request.body) sku = sanitizeNumber(int(body['sku'])) print(sku) - # find target inventory - target = qa_collection.find_one({ 'sku': sku }) - if not target: - return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) - - # pull link from inventory db - print(target['link']) - # look up that link and save it to blob container on Azure + # # find target inventory + # target = qa_collection.find_one({ 'sku': sku }) + # if not target: + # return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) + + # # generate user agent + # uaArr = generate_ua() + # headers = { + # 'User-Agent': f'user-agent={random.choice(uaArr)}', # Add your user-agent string here + # 'Accept-Language': 'en-US,en;q=0.9', + # } + # # get raw html and parse it with scrapy + # rawHTML = requests.get(url=target['link'], headers=headers).text + # response = HtmlResponse(url=target['link'], body=rawHTML, encoding='utf-8') + + # # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text + # integer = response.selector.xpath('//span[has-class("a-price-whole")]/text()').extract()[0] + # decimal = response.selector.xpath('//span[has-class("a-price-fraction")]/text()').extract()[0] + # res = float(integer + '.' + decimal) - return Response(target['link'], status.HTTP_200_OK) \ No newline at end of file + # if not res: + # return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) + # return Response(res, status.HTTP_200_OK) \ No newline at end of file diff --git a/inventoryController/urls.py b/inventoryController/urls.py index c8c242a..664d1fe 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -10,5 +10,7 @@ path("getInventoryByOwnerId/", views.getInventoryByOwnerId, name="getInventoryByOwnerId"), path("getInventoryInfoByOwnerId", views.getInventoryInfoByOwnerId, name="getInventoryInfoByOwnerId"), path("getAllShelfLocations", views.getAllShelfLocations, name="getAllShelfLocations"), - path("generateDescriptionBySku", views.generateDescriptionBySku, name="generateDescriptionBySku") + path("generateDescriptionBySku", views.generateDescriptionBySku, name="generateDescriptionBySku"), + path("scrapePriceBySkuAmazon", views.scrapePriceBySkuAmazon, name="scrapePriceBySkuAmazon"), + path("scrapePriceBySkuHomeDepot", views.scrapePriceBySkuHomeDepot, name="scrapePriceBySkuHomeDepot"), ] diff --git a/inventoryController/views.py b/inventoryController/views.py index 1a5030e..e6832fb 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -1,14 +1,17 @@ +import random +import requests from time import time, ctime +from scrapy.http import HtmlResponse from datetime import datetime, timedelta -from django.views.decorators.csrf import csrf_exempt from inventoryController.models import InventoryItem -from CCPDController.utils import decodeJSON, get_db_client, sanitizeSku, convertToTime, get_client_ip +from CCPDController.scrape_utils import generate_ua +from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework import status +from fake_useragent import UserAgent from bson.objectid import ObjectId from collections import Counter from CCPDController.chat_gpt_utils import generate_short_product_title, generate_full_product_title @@ -18,6 +21,7 @@ db = get_db_client() qa_collection = db['Inventory'] user_collection = db['User'] +ua = UserAgent() # query param sku for inventory db row # sku: string @@ -256,6 +260,8 @@ def getAllShelfLocations(): # description: string @api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) def generateDescriptionBySku(request): try: body = decodeJSON(request.body) @@ -273,4 +279,95 @@ def generateDescriptionBySku(request): lead = generate_short_product_title('USED LIKE NEW - MISISNG 2 ACCESSORIES - Double Canister Dry Food Dispenser Convenient Storage') # full_lead = generate_full_product_title(comment['comment'], '') - return Response(lead, status.HTTP_200_OK) \ No newline at end of file + return Response(lead, status.HTTP_200_OK) + +# return mrsp from amazon for given sku +# sku: string +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def scrapePriceBySkuAmazon(request): + try: + body = decodeJSON(request.body) + sku = sanitizeNumber(int(body['sku'])) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + # find target inventory + target = qa_collection.find_one({ 'sku': sku }) + if not target: + return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) + + # extract url incase where the link includes Amazon title + url = target['link'] + start_index = target['link'].find("https://") + if start_index != -1: + url = target['link'][start_index:] + print("Extracted URL:", url) + + # generate header with random user agent + headers = { + 'User-Agent': f'user-agent={ua.random}', + 'Accept-Language': 'en-US,en;q=0.9', + } + + # get raw html and parse it with scrapy + # TODO: purchase and implement proxy service + rawHTML = requests.get(url=url, headers=headers).text + response = HtmlResponse(url=url, body=rawHTML, encoding='utf-8') + + # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text + integer = response.selector.xpath('//span[has-class("a-price-whole")]/text()').extract()[0] + decimal = response.selector.xpath('//span[has-class("a-price-fraction")]/text()').extract()[0] + price = float(integer + '.' + decimal) + + if not price: + return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(price, status.HTTP_200_OK) + +# return mrsp from home depot for given sku +# sku: string +@api_view(['GET']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def scrapePriceBySkuHomeDepot(request): + try: + body = decodeJSON(request.body) + sku = sanitizeNumber(int(body['sku'])) + except: + return Response('Invalid SKU', status.HTTP_400_BAD_REQUEST) + + # find target inventory + target = qa_collection.find_one({ 'sku': sku }) + if not target: + return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) + + # extract url incase where the link includes title + url = target['link'] + start_index = target['link'].find("https://") + if start_index != -1: + url = target['link'][start_index:] + print("Extracted URL:", url) + + # generate header with random user agent + headers = { + 'User-Agent': f'user-agent={ua.random}', + 'Accept-Language': 'en-US,en;q=0.9', + } + + # get raw html and parse it with scrapy + # TODO: purchase and implement proxy service + rawHTML = requests.get(url=url, headers=headers).text + response = HtmlResponse(url=url, body=rawHTML, encoding='utf-8') + + + # HD Canada className = hdca-product__description-pricing-price-value + # HD US className = ???? + + # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text + price = response.selector.xpath('//span[has-class("hdca-product__description-pricing-price-value")]/text()').extract() + # price = price[0].replace('$', '') + + if not price: + return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(price, status.HTTP_200_OK) diff --git a/requirements.txt b/requirements.txt index 62b597f52209b8468784c4070401329138411982..d64a33e3076afe473f061544e556362592a38f48 100644 GIT binary patch literal 3914 zcmZvf-ESIK5XJ9vrT!^c0Xt6fkcUcss#?{OBjwTJV-47Np$nA6e|+2Xo3q1Rk_rJf zGj~4DoSC`%??2Ph>a{J0GS#~&i!##px3bgU`|?Szcjciw?#Tz-;PE9qJ%klpC_ z$w0O*OUXK&eG2)tWFJDuXAiR7^>!x(X+8aGIZC=MuMr8>Cm|h21nK=vTG$_zxpesp z&2d=zsLz`|%Br^ytDUSaF%X{6dpHmf{e!z z*L8TBNp1-^@t*X*ABDt5cxHUad}b~moub0SI^+|bG#*CFHs+G21{sbAb(oix;snb^ zmPS#-F0k9k=398cGZ^zWese{%7IH7)m8`M*AtDA(kO3)bz-eM8CK0RLxRYcjJIIr#L#=UN_l2?} zzD|Bq4lA7fU3rekm%TLE>o^tzX|iiCWLNS!RUN6BJA!i+)5~*B`6~a7@YyStZG00+ za@`ELJadffJNYt4!M64u-W>nssZ&Js616@GUt1Le zPDfc0KpS>efAj(zfu+e|$K+4^!L~-PHZ<)MDLFgeWne-VkXgp;36hzuq&q&xJIr~d zJPykF`{{#y=ad>>&*vwDzNHJ8t)1dq%ey;q5}9Jdm0qhVt*`+DqIXxJPiHkz8CWuv z6=Uu>Hnh;nK2f$YLzcaskw`DhwuP*~*kN^y)H;4=7NA1~i97X}9%%>3Yt-ekQ#`d& z?>XXzF?d{sB{XVnp2dQUm^yoUy1LcMDNxAyLOfV`e~xFP9wZth{AwSt;}3a;&CK%$ z{r%MEZS-nz9w9<(qy`nHexB}FsA#cG4pABUE2B@2 z?vy_rXkmA%IC9>$}7&1H!P{QvwCww{-<(=nGbsX-rE3` zOJvxzTWv#c%koQUS||D8n$L0mDvx%mTe4Hi3r;%G0E)?OTmuaL5<-pH+i3-Q!mdPl!b3- zwO?=hn{ZAt?l9hCoE_dj=|iw&C)c}Hn7J1$QY-a>3EW*DrmsMHC<2_nL06StMj z_I=z__Cj?ddCoheFaryoW_aWF#chJ@iZKnTzKwb_Bcbx{uC%=E;^fw;yk>o~Oyv zlTO#3K}J2iF~2Ay_A<{cbSW5^OWB{Nn3voM5Dz$VyPD05ujqSvF ldrmo Date: Sat, 20 Jan 2024 19:07:28 -0500 Subject: [PATCH 047/107] worked on scraping logic --- CCPDController/scrape_utils.py | 258 +++++++++++++-------------------- README.md | 3 + imageController/views.py | 15 +- inventoryController/urls.py | 2 +- inventoryController/views.py | 64 +++++--- 5 files changed, 148 insertions(+), 194 deletions(-) diff --git a/CCPDController/scrape_utils.py b/CCPDController/scrape_utils.py index 534c42d..6dd1ef6 100644 --- a/CCPDController/scrape_utils.py +++ b/CCPDController/scrape_utils.py @@ -1,161 +1,107 @@ -# ''' -# Scrape stock images URL from Amazon, HomeDepot etc. -# Original Author: ouyangxue-0407 -# ''' -# import time -# import random -# import requests -# from PIL import Image -# from io import BytesIO +import re +import time +import random +import requests from fake_useragent import UserAgent -# from selenium import webdriver -# from selenium.webdriver.common.by import By -# from selenium.webdriver.chrome.options import Options - -# # random wait time between scrape -# random_wait = lambda : time.sleep(random.randint(30, 50)) - -# # # Amazon US -# # def search_amazon(self: str): -# # url = f"https://www.amazon.com/s?k={self.query}" -# # page = self.context.new_page() -# # page.goto(url) -# # page.wait_for_load_state('domcontentloaded') -# # first_product_link = page.query_selector("a.a-link-normal") -# # if first_product_link: -# # first_product_img = first_product_link.query_selector("img") -# # if first_product_img: -# # img_src = first_product_img.get_attribute('src') -# # return img_src -# # return None - -# # # Amazon CA -# # def search_amazon_ca(query: str): -# # play = sync_playwright().start() -# # browser = play.chromium.launch( -# # headless=True, -# # channel="msedge", -# # args=[ -# # '--enable-automation', -# # '--window-size=1280,768', -# # ], -# # ) -# # context = browser.new_context() -# # url = f"https://www.amazon.ca/s?k={query}" -# # page = context.new_page() -# # page.goto(url) -# # page.wait_for_load_state('domcontentloaded') + +# Amazon scrapping utils +# center col div id and class name +centerColId = 'centerCol' +centerColClass = 'centerColAlign' + +# right col tag id and class name +rightColId = 'rightCol' +rightColClass = rightColId + +# product title span id +productTitleId = 'productTitle' + +# price tag class +priceWholeClass = 'a-price-whole' +priceFractionClass = 'a-price-fraction' +priceRangeClass = 'a-price-range' + +# image container class +imageContainerClass = 'imgTagWrapper' + + +# random wait time between scrape +random_wait = lambda : time.sleep(random.randint(30, 50)) + +# extract url from string using regex +def extract_urls(input): + words = input.split() + regex = re.compile(r'https?://\S+') + return [word for word in words if regex.match(word)][0] + +# takes scrapy HtmlResponse generated from rawHTML +# return array of center col's children tags +def getCenterCol(response): + # return error if no center col found + children = response.xpath(f'//div[@id="{centerColId}" or @class="{centerColClass}"]/child::*') + if len(children) < 1: + raise Exception('No center column found') + return children + +def getRightCol(response): + # id = "unqualifiedBuyBox" + children = response.xpath(f'//div[@id="{rightColId}" or @class="{rightColClass}"]/child::*') + if len(children) < 1: + raise Exception('No right column found') + return children + +# takes scrapy HtmlResponse object and returns title +# Amazon CA +def getTitle(response) -> str: + arr = getCenterCol(response) -# # # start scraping -# # first_product_link = page.query_selector("a.a-link-normal") -# # if not first_product_link: -# # return None + # get title + # remove whitespace around it + for p in arr.xpath(f'//span[@id="{productTitleId}"]/text()'): + title = p.extract().strip() + return title + +# takes rawHTML str and returns +# mrsp in float +# mrsp range in array of float +# or price unavailable string +# Amazon CA +def getMrsp(response): + arr = getCenterCol(response) + + right = getRightCol(response) + # if 'unqualifiedBuyBox' appears in arr for more than 2 times, return unavailable + unqualifiedBox = right.xpath('//div[@id="unqualifiedBuyBox"]').getall()[:4] + if len(unqualifiedBox) > 2: + return 'Currently unavailable' + + + # grab price in span tag + # mrsp whole joint by fraction + integer = arr.xpath(f'//span[has-class("{priceWholeClass}")]/text()').extract() + decimal = arr.xpath(f'//span[has-class("{priceFractionClass}")]/text()').extract() + if integer and decimal: + price = float(integer[0] + '.' + decimal[0]) + return price -# # first_product_img = first_product_link.query_selector("img") -# # if not first_product_img: -# # return None + # extract price range if no fixed price + price = [] + rangeTag = arr.xpath(f'//span[@class="{priceRangeClass}"]/child::*') + for p in rangeTag.xpath('//span[@data-a-color="price" or @class="a-offscreen"]/text()').extract(): + if '$' in p and p not in price: + price.append(p) + return price + +# takes scrapy response and get full quality stock image +# src is the low quality image +# Amazon CA +def getImageUrl(response): + # id="imgTagWrapperId" class="imgTagWrapper" + img = response.xpath(f'//div[@class="{imageContainerClass}"]/child::*').extract_first() + if img: + http_pattern = re.compile(r'https?://\S+') + res = http_pattern.findall(img) + # return res[:2] # for both lq and hq image + return res[1] -# # img_src = first_product_img.get_attribute('src') -# # browser.close() -# # return img_src - -# # # HomeDepot US -# # def search_homedepot(query: str): -# # url = f"https://www.homedepot.com/s/{query}" -# # page = self.context.new_page() -# # page.goto(url) -# # page.wait_for_load_state('domcontentloaded') -# # first_product_link = page.query_selector("a.product-image") -# # if first_product_link: -# # first_product_img = first_product_link.query_selector("img") -# # if first_product_img: -# # img_src = first_product_img.get_attribute('src') -# # return img_src -# # return None - -# # # HomeDepot CA -# # def search_homedepot_ca(query: str): -# # url = f"https://www.homedepot.ca/search?q={query}" -# # page = self.context.new_page() -# # page.goto(url) -# # page.wait_for_load_state('domcontentloaded') -# # first_product_link = page.query_selector("a.acl-product-card__image-link") -# # if first_product_link: -# # first_product_img = first_product_link.query_selector("img") -# # if first_product_img: -# # img_src = first_product_img.get_attribute('src') -# # return img_src -# # return None - -# generate 10 Random user agent -def generate_ua(): - ua = UserAgent() - user_agents = [] - for i in range(10): - user_agent = ua.random - user_agents.append(user_agent) - return user_agents - -# # user agent array -# uaArr = generate_ua() - -# # selenium webdriver options -# options = webdriver.ChromeOptions() -# options.add_argument("--disable-blink-features=AutomationControlled") -# options.add_argument("--disable-extensions") -# options.add_argument("--profile-directory=Default") -# options.add_argument("--disable-plugins-discovery") -# options.add_argument("--lang=en") -# options.add_argument(f'user-agent={random.choice(uaArr)}') - - - -# def create_driver(user_agent): -# options = Options() -# options = webdriver.ChromeOptions() -# options.add_argument("--disable-blink-features=AutomationControlled") -# options.add_argument("--disable-extensions") -# options.add_argument("--profile-directory=Default") -# options.add_argument("--disable-plugins-discovery") -# options.add_argument("--lang=en") -# options.add_argument(f'user-agent={random.choice(user_agent)}') -# options.binary_location = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" -# options.add_experimental_option('excludeSwitches', ['enable-logging']) -# driver = webdriver.Chrome(options=options, executable_path='d:\\CCP-AUTOMATION\\chromedriver.exe') -# return driver - -# def scrape(url: str): -# driver = webdriver.Chrome(options=options) -# driver.get(url) -# div_elements = driver.find_elements(By.CSS_SELECTOR, "div.mediagallery__mainimage") -# if div_elements: -# img_element = div_elements[0].find_element("tag name", "img") -# img_url = img_element.get_attribute("src") -# response = requests.get(img_url) -# img_bytes = BytesIO(response.content) -# img = Image.open(img_bytes) -# img.save(os.path.join(target_folder, f"{item}.jpg")) -# return driver -# #except Except: -# else: -# div_elements = driver.find_elements(By.CSS_SELECTOR, "div.canvas-container") -# if div_elements: -# button=div_elements[0].find_element(By.CLASS_NAME, "modal-button") -# button.click() -# div_img_elements = div_elements[0].find_elements(By.XPATH, "//div[contains(@class, 'image-container') and contains(@class, 'ng-star-inserted')]") -# img_element = div_img_elements[0].find_element("tag name", "img") -# img_url = img_element.get_attribute("src") -# try: -# with urllib.request.urlopen(url, timeout=5) as response: -# image_content = response.read() -# with open(os.path.join(target_folder, f"{item}.jpg"), "wb") as file: -# file.write(image_content) -# return driver -# except: -# print("No Image Found Time Out") -# return driver -# else: -# print("No Image Found") -# return driver -# return None diff --git a/README.md b/README.md index 8e2be66..2e20be6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ +[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%203.svg)](https://www.digitalocean.com/?refcode=3b0d0ab4927b&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) + ## Services - Q&A inventory form controller. - Inventory image gallery controller. @@ -30,3 +32,4 @@ python manage.py check --deploy # Generate requirement.txt pip freeze > requirements.txt ``` + diff --git a/imageController/views.py b/imageController/views.py index 2694709..e6978ea 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -15,7 +15,6 @@ from CCPDController.utils import decodeJSON, getNDayBefore, sanitizeNumber, sanitizeString, getBlobTimeString, get_db_client from CCPDController.authentication import JWTAuthentication from CCPDController.permissions import IsQAPermission, IsAdminPermission -from CCPDController.scrape_utils import generate_ua from dotenv import load_dotenv from urllib import parse import pymongo @@ -171,16 +170,4 @@ def scrapeStockImageBySku(request): # 'User-Agent': f'user-agent={random.choice(uaArr)}', # Add your user-agent string here # 'Accept-Language': 'en-US,en;q=0.9', # } - - # # get raw html and parse it with scrapy - # rawHTML = requests.get(url=target['link'], headers=headers).text - # response = HtmlResponse(url=target['link'], body=rawHTML, encoding='utf-8') - - # # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text - # integer = response.selector.xpath('//span[has-class("a-price-whole")]/text()').extract()[0] - # decimal = response.selector.xpath('//span[has-class("a-price-fraction")]/text()').extract()[0] - # res = float(integer + '.' + decimal) - - # if not res: - # return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) - # return Response(res, status.HTTP_200_OK) \ No newline at end of file + \ No newline at end of file diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 664d1fe..17e59ac 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -11,6 +11,6 @@ path("getInventoryInfoByOwnerId", views.getInventoryInfoByOwnerId, name="getInventoryInfoByOwnerId"), path("getAllShelfLocations", views.getAllShelfLocations, name="getAllShelfLocations"), path("generateDescriptionBySku", views.generateDescriptionBySku, name="generateDescriptionBySku"), - path("scrapePriceBySkuAmazon", views.scrapePriceBySkuAmazon, name="scrapePriceBySkuAmazon"), + path("scrapeInfoBySkuAmazon", views.scrapeInfoBySkuAmazon, name="scrapeInfoBySkuAmazon"), path("scrapePriceBySkuHomeDepot", views.scrapePriceBySkuHomeDepot, name="scrapePriceBySkuHomeDepot"), ] diff --git a/inventoryController/views.py b/inventoryController/views.py index e6832fb..cd01ad1 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -1,10 +1,11 @@ import random +import re import requests from time import time, ctime from scrapy.http import HtmlResponse from datetime import datetime, timedelta from inventoryController.models import InventoryItem -from CCPDController.scrape_utils import generate_ua +from CCPDController.scrape_utils import extract_urls, getImageUrl, getMrsp, getTitle from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication @@ -286,7 +287,7 @@ def generateDescriptionBySku(request): @api_view(['GET']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) -def scrapePriceBySkuAmazon(request): +def scrapeInfoBySkuAmazon(request): try: body = decodeJSON(request.body) sku = sanitizeNumber(int(body['sku'])) @@ -298,32 +299,42 @@ def scrapePriceBySkuAmazon(request): if not target: return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) - # extract url incase where the link includes Amazon title - url = target['link'] - start_index = target['link'].find("https://") - if start_index != -1: - url = target['link'][start_index:] - print("Extracted URL:", url) + # return error if not amazon link or not http + link = target['link'] + if 'amazon' not in link or 'http' not in link: + return Response('Invalid URL', status.HTTP_400_BAD_REQUEST) + + # extract the first http url + link = extract_urls(link) # generate header with random user agent headers = { 'User-Agent': f'user-agent={ua.random}', 'Accept-Language': 'en-US,en;q=0.9', } - - # get raw html and parse it with scrapy - # TODO: purchase and implement proxy service - rawHTML = requests.get(url=url, headers=headers).text - response = HtmlResponse(url=url, body=rawHTML, encoding='utf-8') + print(headers) - # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text - integer = response.selector.xpath('//span[has-class("a-price-whole")]/text()').extract()[0] - decimal = response.selector.xpath('//span[has-class("a-price-fraction")]/text()').extract()[0] - price = float(integer + '.' + decimal) + # get raw html and parse it with scrapy + # TODO: use 10 proxy service to incraese scraping speed + payload = { + 'title': '', + 'mrsp': '', + 'imgUrl': '' + } - if not price: - return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(price, status.HTTP_200_OK) + # request the raw html from Amazon + rawHTML = requests.get(url=link, headers=headers).text + response = HtmlResponse(url=link, body=rawHTML, encoding='utf-8') + + try: + # call functions from scrape_utils + # it throws exception if no center or right column + payload['title'] = getTitle(response) + payload['mrsp'] = getMrsp(rawHTML, response) + payload['imgUrl'] = getImageUrl(response) + except: + return Response('Scrape Failed, Cannot Find Component', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(payload, status.HTTP_200_OK) # return mrsp from home depot for given sku # sku: string @@ -342,8 +353,12 @@ def scrapePriceBySkuHomeDepot(request): if not target: return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) - # extract url incase where the link includes title + # check if url is home depot url = target['link'] + if 'homedepot' not in url or 'http' not in url: + return Response('Invalid URL', status.HTTP_400_BAD_REQUEST) + + # extract url incase where the link includes title start_index = target['link'].find("https://") if start_index != -1: url = target['link'][start_index:] @@ -360,12 +375,15 @@ def scrapePriceBySkuHomeDepot(request): rawHTML = requests.get(url=url, headers=headers).text response = HtmlResponse(url=url, body=rawHTML, encoding='utf-8') - # HD Canada className = hdca-product__description-pricing-price-value + # HD Canada itemprop="price" + # 44.98 # HD US className = ???? + + # grab the fist span element encountered tagged with class 'a-price-whole' and extract the text - price = response.selector.xpath('//span[has-class("hdca-product__description-pricing-price-value")]/text()').extract() + price = response.selector.xpath('//span/text()').extract() # price = price[0].replace('$', '') if not price: From a453c23c3b96a8b9dec84693dcb8e44c8e2ec808 Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Tue, 23 Jan 2024 19:02:06 -0500 Subject: [PATCH 048/107] fixed amazon scrape code - fixed scrape code logic - work on inventory time format problem (change to ISO format) --- CCPDController/scrape_utils.py | 35 +++++++++-------- CCPDController/utils.py | 20 +++++++--- imageController/urls.py | 1 - imageController/views.py | 26 +------------ inventoryController/urls.py | 1 + inventoryController/views.py | 68 +++++++++++++++++++++++++++------ requirements.txt | Bin 3914 -> 3992 bytes 7 files changed, 94 insertions(+), 57 deletions(-) diff --git a/CCPDController/scrape_utils.py b/CCPDController/scrape_utils.py index 6dd1ef6..41a1deb 100644 --- a/CCPDController/scrape_utils.py +++ b/CCPDController/scrape_utils.py @@ -5,13 +5,15 @@ from fake_useragent import UserAgent # Amazon scrapping utils +# works for both Amazon CA nad US + # center col div id and class name centerColId = 'centerCol' centerColClass = 'centerColAlign' # right col tag id and class name rightColId = 'rightCol' -rightColClass = rightColId +rightColClass = 'rightCol' # product title span id productTitleId = 'productTitle' @@ -51,7 +53,6 @@ def getRightCol(response): return children # takes scrapy HtmlResponse object and returns title -# Amazon CA def getTitle(response) -> str: arr = getCenterCol(response) @@ -61,32 +62,38 @@ def getTitle(response) -> str: title = p.extract().strip() return title -# takes rawHTML str and returns -# mrsp in float -# mrsp range in array of float -# or price unavailable string -# Amazon CA +# takes rawHTML str and returns: +# - mrsp in float +# - mrsp range in array of float +# - or price unavailable string def getMrsp(response): - arr = getCenterCol(response) - + center = getCenterCol(response) right = getRightCol(response) + + + # check for out of stock id in right col + outOfStock = right.xpath('//div[@id="outOfStock"]').getall() + if len(outOfStock) > 1: + return 'Currently unavailable' + # if 'unqualifiedBuyBox' appears in arr for more than 2 times, return unavailable unqualifiedBox = right.xpath('//div[@id="unqualifiedBuyBox"]').getall()[:4] if len(unqualifiedBox) > 2: return 'Currently unavailable' - + + # Currently unavailable in right col set # grab price in span tag # mrsp whole joint by fraction - integer = arr.xpath(f'//span[has-class("{priceWholeClass}")]/text()').extract() - decimal = arr.xpath(f'//span[has-class("{priceFractionClass}")]/text()').extract() + integer = center.xpath(f'//span[has-class("{priceWholeClass}")]/text()').extract() + decimal = center.xpath(f'//span[has-class("{priceFractionClass}")]/text()').extract() if integer and decimal: price = float(integer[0] + '.' + decimal[0]) return price # extract price range if no fixed price price = [] - rangeTag = arr.xpath(f'//span[@class="{priceRangeClass}"]/child::*') + rangeTag = center.xpath(f'//span[@class="{priceRangeClass}"]/child::*') for p in rangeTag.xpath('//span[@data-a-color="price" or @class="a-offscreen"]/text()').extract(): if '$' in p and p not in price: price.append(p) @@ -103,5 +110,3 @@ def getImageUrl(response): res = http_pattern.findall(img) # return res[:2] # for both lq and hq image return res[1] - - diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 12a7e0c..0e5b5c4 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -1,10 +1,7 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import os import json -from django.conf import settings import pytz -from rest_framework.response import Response -from rest_framework import exceptions from pymongo import MongoClient from dotenv import load_dotenv load_dotenv() @@ -49,7 +46,11 @@ def get_client_ip(request): user_time_format = "%b %-d %Y" # Q&A table time filter format -filter_time_format = "%Y-%m-%dT%H:%M:%S.%fZ" + +# TODO: change from zulu to est (-5:00) +# filter_time_format = "%Y-%m-%dT%H:%M:%S.%fZ" +filter_time_format = "%Y-%m-%dT%H:%M:%S.%f-05:00" + # image blob date format blob_date_format = "%a %b %d %Y" @@ -60,6 +61,7 @@ def getBlobTimeString() -> str: current_time = datetime.now(eastern_timezone) return current_time.strftime(blob_date_format) +# return N days before time_str in blob date format def getNDayBefore(days_before, time_str) -> str: blob_time = datetime.strptime(time_str, blob_date_format) blob_time = blob_time - timedelta(days=days_before) @@ -73,6 +75,14 @@ def getNDayBefore(days_before, time_str) -> str: def convertToTime(time_str): return datetime.strptime(time_str, time_format) +# inventory time format in eastern timezone +def getIsoFormatNow(): + eastern_timezone = pytz.timezone('America/Toronto') + current_time = datetime.now(eastern_timezone) + now = current_time.isoformat() + return now + + # check if body contains valid user registration information def checkBody(body): if not inRange(body['name'], min_name, max_name): diff --git a/imageController/urls.py b/imageController/urls.py index 92a5567..0931e75 100644 --- a/imageController/urls.py +++ b/imageController/urls.py @@ -7,5 +7,4 @@ path("getUrlsByOwner", views.getUrlsByOwner, name="getUrlsByOwner"), path("getUrlsBySku", views.getUrlsBySku, name="getUrlsBySku"), path("deleteImageByName", views.deleteImageByName, name="deleteImageByName"), - path("scrapeStockImageBySku", views.scrapeStockImageBySku, name="scrapeStockImageBySku"), ] diff --git a/imageController/views.py b/imageController/views.py index e6978ea..4ec7ae0 100644 --- a/imageController/views.py +++ b/imageController/views.py @@ -6,7 +6,6 @@ from PIL import Image import datetime from datetime import timedelta -from time import time, ctime from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceExistsError from rest_framework import status @@ -147,27 +146,4 @@ def deleteImageByName(request): res = product_image_container_client.delete_blob(imageName) except: return Response('No Such Image', status.HTTP_404_NOT_FOUND) - return Response('Image Deleted', status.HTTP_200_OK) - -# pull one stock image from website and save it to blob container under that sku -# sku: str -@api_view(['POST']) -@authentication_classes([JWTAuthentication]) -@permission_classes([IsAdminPermission]) -def scrapeStockImageBySku(request): - body = decodeJSON(request.body) - sku = sanitizeNumber(int(body['sku'])) - print(sku) - - # # find target inventory - # target = qa_collection.find_one({ 'sku': sku }) - # if not target: - # return Response('No Such Inventory', status.HTTP_404_NOT_FOUND) - - # # generate user agent - # uaArr = generate_ua() - # headers = { - # 'User-Agent': f'user-agent={random.choice(uaArr)}', # Add your user-agent string here - # 'Accept-Language': 'en-US,en;q=0.9', - # } - \ No newline at end of file + return Response('Image Deleted', status.HTTP_200_OK) \ No newline at end of file diff --git a/inventoryController/urls.py b/inventoryController/urls.py index 17e59ac..046f2cc 100644 --- a/inventoryController/urls.py +++ b/inventoryController/urls.py @@ -13,4 +13,5 @@ path("generateDescriptionBySku", views.generateDescriptionBySku, name="generateDescriptionBySku"), path("scrapeInfoBySkuAmazon", views.scrapeInfoBySkuAmazon, name="scrapeInfoBySkuAmazon"), path("scrapePriceBySkuHomeDepot", views.scrapePriceBySkuHomeDepot, name="scrapePriceBySkuHomeDepot"), + path("sendCSV", views.sendCSV, name="sendCSV"), ] diff --git a/inventoryController/views.py b/inventoryController/views.py index cd01ad1..a8909f9 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -1,12 +1,11 @@ -import random +import os import re import requests -from time import time, ctime from scrapy.http import HtmlResponse from datetime import datetime, timedelta from inventoryController.models import InventoryItem from CCPDController.scrape_utils import extract_urls, getImageUrl, getMrsp, getTitle -from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime +from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime, getIsoFormatNow from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -17,6 +16,8 @@ from collections import Counter from CCPDController.chat_gpt_utils import generate_short_product_title, generate_full_product_title import pymongo +import pandas as pd +from bs4 import BeautifulSoup # pymongo db = get_db_client() @@ -301,18 +302,17 @@ def scrapeInfoBySkuAmazon(request): # return error if not amazon link or not http link = target['link'] - if 'amazon' not in link or 'http' not in link: + link = "https://www.amazon.ca/Brightwell-Aquatics-AminOmega-Supplement-Aquaria/dp/B001DCV0ZI/ref=dp_fod_sccl_1/138-3560016-0966344?pd_rd_w=qG68t&content-id=amzn1.sym.e943220f-f90d-493f-b9ae-af4c3e457137&pf_rd_p=e943220f-f90d-493f-b9ae-af4c3e457137&pf_rd_r=HAX5TTWXAFJYSH2XSWVJ&pd_rd_wg=kvVBw&pd_rd_r=a446f094-3404-4173-abbc-8200b7f4c00e&pd_rd_i=B001DCV1PC&th=1" + if 'amazon' not in link and 'http' not in link and 'a.co' not in link: return Response('Invalid URL', status.HTTP_400_BAD_REQUEST) # extract the first http url link = extract_urls(link) - # generate header with random user agent headers = { 'User-Agent': f'user-agent={ua.random}', 'Accept-Language': 'en-US,en;q=0.9', } - print(headers) # get raw html and parse it with scrapy # TODO: use 10 proxy service to incraese scraping speed @@ -325,15 +325,18 @@ def scrapeInfoBySkuAmazon(request): # request the raw html from Amazon rawHTML = requests.get(url=link, headers=headers).text response = HtmlResponse(url=link, body=rawHTML, encoding='utf-8') - try: - # call functions from scrape_utils - # it throws exception if no center or right column payload['title'] = getTitle(response) - payload['mrsp'] = getMrsp(rawHTML, response) + except: + return Response('Failed to Get Title', status.HTTP_500_INTERNAL_SERVER_ERROR) + try: + payload['mrsp'] = getMrsp(response) + except: + return Response('Failed to Get MRSP', status.HTTP_500_INTERNAL_SERVER_ERROR) + try: payload['imgUrl'] = getImageUrl(response) except: - return Response('Scrape Failed, Cannot Find Component', status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response('No Image URL Found', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(payload, status.HTTP_200_OK) # return mrsp from home depot for given sku @@ -389,3 +392,46 @@ def scrapePriceBySkuHomeDepot(request): if not price: return Response('No Result', status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(price, status.HTTP_200_OK) + + +@api_view(['POST']) +@authentication_classes([JWTAuthentication]) +@permission_classes([IsAdminPermission]) +def sendCSV(request): + body = decodeJSON(request.body) + # relative path + path = body['path'] + + # joint file location with relative path + dirName = os.path.dirname(__file__) + fileName = os.path.join(dirName, path) + + print(getIsoFormatNow()) + + # parse csv to pandas data frame + data = pd.read_csv(filepath_or_buffer=fileName) + + # loop pandas dataframe + for index in data.head().index: + # time convert to iso format + # original: 12/22/2023 5:54pm + # targeted: 2024-01-03T05:00:00.000 optional: -05:00 (EST is -5) + data['time'][index] = datetime.strptime(data['time'][index], "%m/%d/%Y %I:%M%p").isoformat() + # print(time) + + # remove all html tags + cleanLink = BeautifulSoup(data['link'][index], "lxml").text + + # item condition set to capitalized + itemCondition = data['itemCondition'][index] + + # platform + platform = data['platform'][index] + + + + # set output copy path + # output = os.path.join(dirName, 'output.csv') + # data.to_csv(output) + + return Response(str(data.tail()), status.HTTP_200_OK) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d64a33e3076afe473f061544e556362592a38f48..11b8d3a201e638fa0e328df9acba00751955642a 100644 GIT binary patch delta 67 zcmX>lH$#5IF?P{Bh7^WGhGGU=AT(mo17m~Dd)b+pr4t!S7*ZKZfn+8_4qSl+NYUoi HT=Q7~3m6Vh delta 17 ZcmbOse@bq{G4{<**ncr?=Hu>S0RTrN2N3`O From 213320c0bfd2f2fa10c0cd297ad0162c8eb3d7fc Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Wed, 24 Jan 2024 19:04:11 -0500 Subject: [PATCH 049/107] migrated QA records - Migrated records from SQL to Mongo. - Added CSV processing logic. - Fixed scraping logic. --- CCPDController/utils.py | 2 ++ adminController/views.py | 4 ++-- inventoryController/views.py | 41 ++++++++++++++++++++---------------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/CCPDController/utils.py b/CCPDController/utils.py index 0e5b5c4..9213ebe 100644 --- a/CCPDController/utils.py +++ b/CCPDController/utils.py @@ -16,6 +16,8 @@ def get_db_client(): return db_handle +qa_inventory_db_name = 'QAInventory' + # decode body from json to object decodeJSON = lambda body : json.loads(body.decode('utf-8')) diff --git a/adminController/views.py b/adminController/views.py index 05a5d52..14e9885 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -25,13 +25,13 @@ sanitizeUserInfoBody, user_time_format, sanitizeNumber, - filter_time_format, + qa_inventory_db_name, ) # pymongo db = get_db_client() user_collection = db['User'] -qa_collection = db['Inventory'] +qa_collection = db[qa_inventory_db_name] inv_code_collection = db['Invitations'] instock_collection = db['Instock'] retail_collection = db['Retail'] diff --git a/inventoryController/views.py b/inventoryController/views.py index a8909f9..9739df6 100644 --- a/inventoryController/views.py +++ b/inventoryController/views.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from inventoryController.models import InventoryItem from CCPDController.scrape_utils import extract_urls, getImageUrl, getMrsp, getTitle -from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime, getIsoFormatNow +from CCPDController.utils import decodeJSON, get_db_client, sanitizeNumber, sanitizeSku, convertToTime, getIsoFormatNow, qa_inventory_db_name from CCPDController.permissions import IsQAPermission, IsAdminPermission from CCPDController.authentication import JWTAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -21,7 +21,7 @@ # pymongo db = get_db_client() -qa_collection = db['Inventory'] +qa_collection = db[qa_inventory_db_name] user_collection = db['User'] ua = UserAgent() @@ -302,12 +302,14 @@ def scrapeInfoBySkuAmazon(request): # return error if not amazon link or not http link = target['link'] - link = "https://www.amazon.ca/Brightwell-Aquatics-AminOmega-Supplement-Aquaria/dp/B001DCV0ZI/ref=dp_fod_sccl_1/138-3560016-0966344?pd_rd_w=qG68t&content-id=amzn1.sym.e943220f-f90d-493f-b9ae-af4c3e457137&pf_rd_p=e943220f-f90d-493f-b9ae-af4c3e457137&pf_rd_r=HAX5TTWXAFJYSH2XSWVJ&pd_rd_wg=kvVBw&pd_rd_r=a446f094-3404-4173-abbc-8200b7f4c00e&pd_rd_i=B001DCV1PC&th=1" - if 'amazon' not in link and 'http' not in link and 'a.co' not in link: + if 'https' not in link and '.ca' not in link and '.com' not in link: return Response('Invalid URL', status.HTTP_400_BAD_REQUEST) + if 'a.co' not in link and 'amazon' not in link: + return Response('Invalid URL, Not Amazon URL', status.HTTP_400_BAD_REQUEST) # extract the first http url link = extract_urls(link) + # generate header with random user agent headers = { 'User-Agent': f'user-agent={ua.random}', @@ -399,39 +401,42 @@ def scrapePriceBySkuHomeDepot(request): @permission_classes([IsAdminPermission]) def sendCSV(request): body = decodeJSON(request.body) - # relative path path = body['path'] # joint file location with relative path dirName = os.path.dirname(__file__) fileName = os.path.join(dirName, path) - print(getIsoFormatNow()) # parse csv to pandas data frame data = pd.read_csv(filepath_or_buffer=fileName) # loop pandas dataframe - for index in data.head().index: + for index in data.index: # time convert to iso format # original: 12/22/2023 5:54pm # targeted: 2024-01-03T05:00:00.000 optional: -05:00 (EST is -5) - data['time'][index] = datetime.strptime(data['time'][index], "%m/%d/%Y %I:%M%p").isoformat() - # print(time) + time = datetime.strptime(data['time'][index], "%m/%d/%Y %I:%M%p").isoformat() + data.loc[index, 'time'] = time # remove all html tags - cleanLink = BeautifulSoup(data['link'][index], "lxml").text + # if link containes '<' + if '<' in data['link'][index]: + cleanLink = BeautifulSoup(data['link'][index], "lxml").text + data.loc[index, 'link'] = cleanLink # item condition set to capitalized - itemCondition = data['itemCondition'][index] + itemCondition = str(data['itemCondition'][index]).title() + data.loc[index, 'itemCondition'] = itemCondition - # platform - platform = data['platform'][index] + # platform other capitalize + if data['platform'][index] == 'other': + data.loc[index, 'platform'] = 'Other' - + # capitalize one of our employee's name + if data['ownerName'][index] == 'yuxiang si': + data.loc[index, 'ownerName'] = 'Yuxiang Si' # set output copy path - # output = os.path.join(dirName, 'output.csv') - # data.to_csv(output) - - return Response(str(data.tail()), status.HTTP_200_OK) \ No newline at end of file + data.to_csv(path_or_buf='./output.csv', encoding='utf-8', index=False) + return Response(str(data), status.HTTP_200_OK) \ No newline at end of file From 43a41dda2dcd8dc3a986de1677328f9537817aae Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 26 Jan 2024 19:04:17 -0500 Subject: [PATCH 050/107] remove example image, fixed imageController --- adminController/views.py | 7 +++---- examples/11043.jpg | Bin 46574 -> 0 bytes examples/IMG_4374.jpeg | Bin 2155193 -> 0 bytes examples/IMG_4375.jpeg | Bin 1341962 -> 0 bytes imageController/views.py | 11 ++++++++--- inventoryController/views.py | 9 --------- userController/views.py | 3 ++- 7 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 examples/11043.jpg delete mode 100644 examples/IMG_4374.jpeg delete mode 100644 examples/IMG_4375.jpeg diff --git a/adminController/views.py b/adminController/views.py index 14e9885..bcbe406 100644 --- a/adminController/views.py +++ b/adminController/views.py @@ -193,9 +193,7 @@ def updateUserById(request, uid): try: user_collection.update_one( { '_id': userId }, - { - '$set': body - } + { '$set': body } ) except: return Response('Update User Infomation Failed', status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -430,7 +428,8 @@ def getSalesRecordsBySku(request, sku): return Response('No Records Found', status.HTTP_404_NOT_FOUND) return Response(arr, status.HTTP_200_OK) - +# retailRecordId: string +# returnRecord: ReturnRecord @api_view(['POST']) @authentication_classes([JWTAuthentication]) @permission_classes([IsAdminPermission]) diff --git a/examples/11043.jpg b/examples/11043.jpg deleted file mode 100644 index f6088aad3aa43f71303dd4dcfbdfb615c31c2db1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46574 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