From 6d984b40bf9baf77313a74f70088a45f7efdfb05 Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Fri, 22 Jul 2022 02:56:48 +0430 Subject: [PATCH 1/7] first commit --- .gitignore | 129 ++++++++++++++++++++++++++++++++ DjangoCryptoReader/__init__.py | 0 DjangoCryptoReader/asgi.py | 16 ++++ DjangoCryptoReader/settings.py | 124 ++++++++++++++++++++++++++++++ DjangoCryptoReader/urls.py | 21 ++++++ DjangoCryptoReader/wsgi.py | 16 ++++ accounts/__init__.py | 0 accounts/admin.py | 3 + accounts/apps.py | 6 ++ accounts/migrations/__init__.py | 0 accounts/models.py | 3 + accounts/tests.py | 3 + accounts/views.py | 3 + core/__init__.py | 0 core/admin.py | 3 + core/apps.py | 6 ++ core/migrations/__init__.py | 0 core/models.py | 3 + core/tests.py | 3 + core/views.py | 3 + manage.py | 22 ++++++ requirements.txt | 0 22 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100644 DjangoCryptoReader/__init__.py create mode 100644 DjangoCryptoReader/asgi.py create mode 100644 DjangoCryptoReader/settings.py create mode 100644 DjangoCryptoReader/urls.py create mode 100644 DjangoCryptoReader/wsgi.py create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/tests.py create mode 100644 accounts/views.py create mode 100644 core/__init__.py create mode 100644 core/admin.py create mode 100644 core/apps.py create mode 100644 core/migrations/__init__.py create mode 100644 core/models.py create mode 100644 core/tests.py create mode 100644 core/views.py create mode 100644 manage.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b464bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.idea +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/DjangoCryptoReader/__init__.py b/DjangoCryptoReader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoCryptoReader/asgi.py b/DjangoCryptoReader/asgi.py new file mode 100644 index 0000000..f044ac3 --- /dev/null +++ b/DjangoCryptoReader/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for DjangoCryptoReader project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoCryptoReader.settings') + +application = get_asgi_application() diff --git a/DjangoCryptoReader/settings.py b/DjangoCryptoReader/settings.py new file mode 100644 index 0000000..9d71032 --- /dev/null +++ b/DjangoCryptoReader/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for DjangoCryptoReader project. + +Generated by 'django-admin startproject' using Django 4.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-yu!nop*yww+cj=z_&#ed@6b=wcddllspx+fes(i&m248h-chxl' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'DjangoCryptoReader.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'DjangoCryptoReader.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/DjangoCryptoReader/urls.py b/DjangoCryptoReader/urls.py new file mode 100644 index 0000000..8a89c0f --- /dev/null +++ b/DjangoCryptoReader/urls.py @@ -0,0 +1,21 @@ +"""DjangoCryptoReader URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/DjangoCryptoReader/wsgi.py b/DjangoCryptoReader/wsgi.py new file mode 100644 index 0000000..2f1b6fd --- /dev/null +++ b/DjangoCryptoReader/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for DjangoCryptoReader project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoCryptoReader.settings') + +application = get_wsgi_application() diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/core/tests.py b/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/core/views.py b/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..7be8b6e --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoCryptoReader.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 From ab6a97cbc7aced2f8d9b7bbda38106b916138df7 Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Tue, 26 Jul 2022 05:22:36 +0430 Subject: [PATCH 2/7] everything at once! --- .env | 5 + .gitignore | 19 +- Crypto Reader.postman_collection.json | 241 ++++++++++++++++++ DjangoCryptoReader/celery.py | 20 ++ DjangoCryptoReader/settings.py | 82 +++++- DjangoCryptoReader/urls.py | 4 +- Dockerfile | 16 ++ Encryptor/__init__.py | 0 Encryptor/encryptor.py | 56 ++++ Encryptor/hasher_tools.py | 37 +++ accounts/admin.py | 4 +- accounts/management/commands/__init__.py | 0 accounts/management/commands/superuser.py | 13 + accounts/migrations/0001_initial.py | 39 +++ .../migrations/0002_auto_20220722_1554.py | 28 ++ .../0003_remove_user_security_passphrase.py | 17 ++ .../migrations/0004_auto_20220724_0016.py | 33 +++ .../0005_user_auto_update_orders.py | 18 ++ .../migrations/0006_auto_20220726_0002.py | 28 ++ accounts/models.py | 154 ++++++++++- accounts/serializers.py | 60 +++++ accounts/urls.py | 9 + accounts/views.py | 36 ++- core/serializers.py | 51 ++++ core/tasks.py | 15 ++ core/updater.py | 36 +++ core/urls.py | 9 + core/utils.py | 25 ++ core/views.py | 87 ++++++- docker-compose.yml | 84 ++++++ entrypoint.sh | 16 ++ kucoin/KucoinRequestHandler.py | 137 ++++++++++ nginx/Dockerfile | 5 + nginx/nginx.conf | 21 ++ requirements.txt | 15 ++ supervisord.conf | 24 ++ 36 files changed, 1424 insertions(+), 20 deletions(-) create mode 100644 .env create mode 100644 Crypto Reader.postman_collection.json create mode 100644 DjangoCryptoReader/celery.py create mode 100644 Dockerfile create mode 100644 Encryptor/__init__.py create mode 100644 Encryptor/encryptor.py create mode 100644 Encryptor/hasher_tools.py create mode 100644 accounts/management/commands/__init__.py create mode 100644 accounts/management/commands/superuser.py create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/migrations/0002_auto_20220722_1554.py create mode 100644 accounts/migrations/0003_remove_user_security_passphrase.py create mode 100644 accounts/migrations/0004_auto_20220724_0016.py create mode 100644 accounts/migrations/0005_user_auto_update_orders.py create mode 100644 accounts/migrations/0006_auto_20220726_0002.py create mode 100644 accounts/serializers.py create mode 100644 accounts/urls.py create mode 100644 core/serializers.py create mode 100644 core/tasks.py create mode 100644 core/updater.py create mode 100644 core/urls.py create mode 100644 core/utils.py create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 kucoin/KucoinRequestHandler.py create mode 100644 nginx/Dockerfile create mode 100644 nginx/nginx.conf create mode 100644 supervisord.conf diff --git a/.env b/.env new file mode 100644 index 0000000..2bd655b --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +SECRET_KEY=django-insecure-yu!nop*yww+cj=z_&#ed@6b=wcddllspx+fes(i&m248h-chxl +DEBUG=0 +ALLOWED_HOSTS=localhost-127.0.0.1-web +SCHEDULE=30 +salt=$U\xb0\xf5\xbc\x10\xca\xe4\xe3:\\y\r\x91\xb4{ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1b464bd..b7b620e 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,10 @@ target/ profile_default/ ipython_config.py +/static +/media +/Encryptor/SecurityAuthenticator.py +/Encryptor/sym.py # pyenv .python-version @@ -102,12 +106,9 @@ celerybeat.pid *.sage.py # Environments -.env .venv -env/ venv/ ENV/ -env.bak/ venv.bak/ # Spyder project settings @@ -126,4 +127,14 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ + + +#celery beat +celerybeat-schedule.dir +celerybeat-schedule.dat +celerybeat-schedule.bak + +/db +supervisor/supervisord.pid +supervisor/* \ No newline at end of file diff --git a/Crypto Reader.postman_collection.json b/Crypto Reader.postman_collection.json new file mode 100644 index 0000000..af18da8 --- /dev/null +++ b/Crypto Reader.postman_collection.json @@ -0,0 +1,241 @@ +{ + "info": { + "_postman_id": "37e4c9c9-4bba-414a-ac61-2f07fc4cfbf0", + "name": "Crypto Reader", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "10219869" + }, + "item": [ + { + "name": "Accounts", + "item": [ + { + "name": "Register", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "username", + "value": "", + "type": "text" + }, + { + "key": "password", + "value": "", + "type": "text" + }, + { + "key": "re_password", + "value": "", + "type": "text" + }, + { + "key": "kc_pp", + "value": "", + "type": "text" + }, + { + "key": "kc_apikey", + "value": "", + "type": "text" + }, + { + "key": "kc_secret", + "value": "", + "type": "text" + }, + { + "key": "security_passphrase", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{local}}/api/auth/register", + "host": [ + "{{local}}" + ], + "path": [ + "api", + "auth", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "username", + "value": "", + "type": "text" + }, + { + "key": "password", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{local}}/api/auth/login", + "host": [ + "{{local}}" + ], + "path": [ + "api", + "auth", + "login" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Core", + "item": [ + { + "name": "User Orders", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "symbol", + "value": "", + "type": "text" + }, + { + "key": "side", + "value": "", + "type": "text" + }, + { + "key": "type", + "value": "", + "type": "text" + }, + { + "key": "tradeType", + "value": "", + "type": "text" + }, + { + "key": "startAt", + "value": "", + "type": "text" + }, + { + "key": "endAt", + "value": "", + "type": "text" + }, + { + "key": "security_pass_phrase", + "value": "", + "type": "text" + }, + { + "key": "status", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{local}}/api/orders", + "host": [ + "{{local}}" + ], + "path": [ + "api", + "orders" + ] + } + }, + "response": [] + }, + { + "name": "Enable Disable Auto Update", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjU4ODc4MjQ3LCJpYXQiOjE2NTg3OTE4NDcsImp0aSI6IjA2MDNlNzViNDlmMjRhNmM4MDIzZWNhZmUzNjU5OWQwIiwidXNlcl9pZCI6IjdkOGM5Mjk3LTRiNjYtNDE0Yy04ZGUyLTg2YzI2ZTM2ZTdkZiJ9.fygg6SFIi85R5_GsXZQKR7h581KZWUqbDpRLllCKOAs", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{local}}/api/auto_updater", + "host": [ + "{{local}}" + ], + "path": [ + "api", + "auto_updater" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "local", + "value": "localhost", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/DjangoCryptoReader/celery.py b/DjangoCryptoReader/celery.py new file mode 100644 index 0000000..e50f761 --- /dev/null +++ b/DjangoCryptoReader/celery.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery +from decouple import config + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoCryptoReader.settings') + +app = Celery('DjangoCryptoReader') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.conf.beat_schedule = { + 'update_orders': { + 'task': 'core.tasks.update_user_orders', + 'schedule': int(config('SCHEDULE')), + }, + +} + +app.autodiscover_tasks() diff --git a/DjangoCryptoReader/settings.py b/DjangoCryptoReader/settings.py index 9d71032..6240d97 100644 --- a/DjangoCryptoReader/settings.py +++ b/DjangoCryptoReader/settings.py @@ -9,10 +9,12 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ - +from datetime import timedelta from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. +from decouple import config + BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,12 +22,16 @@ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-yu!nop*yww+cj=z_&#ed@6b=wcddllspx+fes(i&m248h-chxl' +SECRET_KEY = config('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True + +DEBUG = bool(int(config('DEBUG'))) ALLOWED_HOSTS = [] +ALLOWED_HOSTS_ENV = config('ALLOWED_HOSTS') +if ALLOWED_HOSTS_ENV: + ALLOWED_HOSTS.extend(ALLOWED_HOSTS_ENV.split('-')) # Application definition @@ -37,6 +43,9 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'accounts.apps.AccountsConfig', + # 'core' ] MIDDLEWARE = [ @@ -71,14 +80,19 @@ WSGI_APPLICATION = 'DjangoCryptoReader.wsgi.application' + # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'crypto_reader', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'db_service', + 'PORT': '5432', + }, } @@ -100,7 +114,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ @@ -116,9 +129,62 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'static' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +AUTH_USER_MODEL = "accounts.User" # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication',) +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=24), + 'REFRESH_TOKEN_LIFETIME': timedelta(hours=24), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': False, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + 'JTI_CLAIM': 'jti', + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1) +} + +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://redis_service:6379/', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} + +CELERY_IMPORTS = ['core.tasks'] +CELERY_BROKER_URL = "redis://redis_service:6379" +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' + + +CACHE_TTL = 60 * 24 * 200 \ No newline at end of file diff --git a/DjangoCryptoReader/urls.py b/DjangoCryptoReader/urls.py index 8a89c0f..3af886e 100644 --- a/DjangoCryptoReader/urls.py +++ b/DjangoCryptoReader/urls.py @@ -14,8 +14,10 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('api/auth/', include('accounts.urls', namespace='Accounts')), + path('api/', include('core.urls', namespace='Core')), ] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f9ce75 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.8.2-slim-buster +ENV PYTHONUNBUFFERED=1 + +WORKDIR /crypto_reader + +COPY requirements.txt requirements.txt + +RUN pip install --upgrade pip +RUN pip install -r requirements.txt + + +COPY . /crypto_reader + +COPY ./entrypoint.sh / + +ENTRYPOINT ["sh","/entrypoint.sh"] \ No newline at end of file diff --git a/Encryptor/__init__.py b/Encryptor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Encryptor/encryptor.py b/Encryptor/encryptor.py new file mode 100644 index 0000000..533e2d4 --- /dev/null +++ b/Encryptor/encryptor.py @@ -0,0 +1,56 @@ +import binascii +from typing import Union +from cryptography.fernet import Fernet +from abc import ABC , abstractmethod + + +class AbsSymmetric(ABC): + + @abstractmethod + def encrypt(self,to_encrypt): + pass + + @abstractmethod + def decrypt(self,to_decrypt): + pass + + +class SymmetricEncryptor(AbsSymmetric): + """ + Upon instantiating we store the kdf which is needed to encrypt and decrypt secrets + """ + + def __init__(self,kdf:bytes): + self.__kdf = kdf + + + def encrypt(self,to_encrypt : str) -> Union[str,bool]: + """ + uses the kdf and encrypts the provided 'to_encrypt' parameter + + :param to_encrypt: the raw data that we want to encrypt like api key etc ... + :return: + """ + try: + f = Fernet(self.__kdf) + must_kept_secret = f.encrypt(bytes(to_encrypt,encoding='utf8')) + except binascii.Error: + return False + except TypeError: + return False + return must_kept_secret.decode("utf-8") + + def decrypt(self,to_decrypt:str) -> Union[str,bool]: + """ + uses the kdf and decrypts the provided 'to_encrypt' parameter + :param to_decrypt: the raw data that we want to decrypt like encrypted api key etc ... + :return: + """ + try: + f = Fernet(self.__kdf) + revealed_secret = f.decrypt(bytes(to_decrypt,encoding='utf8')) + except binascii.Error: + return False + except TypeError: + return False + return revealed_secret.decode("utf-8") \ No newline at end of file diff --git a/Encryptor/hasher_tools.py b/Encryptor/hasher_tools.py new file mode 100644 index 0000000..d786629 --- /dev/null +++ b/Encryptor/hasher_tools.py @@ -0,0 +1,37 @@ +import base64 +from typing import Union +import decouple +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from decouple import config + + + + +def hasher(raw_content:Union[str,bytes]) -> bytes: + """ + The main function to generate a kdf . hashes the given raw_content parameter + :param raw_content: The raw byte or str we want to hash + :return: hashed raw content + """ + raw_content = raw_content if type(raw_content) == str else raw_content.decode('utf-8') + try: + salt = bytes(config("salt"), encoding='utf8') + kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=390000) + hashed_content = base64.urlsafe_b64encode(kdf.derive(bytes(raw_content,encoding='utf-8'))) + except decouple.UndefinedValueError: + raise NotImplementedError({'salt': 'Not found'}) + except SyntaxError as e: + raise SyntaxError(e) + + return hashed_content + +def check_pass_phrase(user,pass_phrase:str) -> bool: + """ + checks if the provided pass phrase is correct with hashing pass phrase and comparing it to the hashed pass phrase in db + :param user: + :param pass_phrase: + :return: + """ + hashed_pass = hasher(pass_phrase) + return hashed_pass.decode('utf-8') == user.security_passphrase \ No newline at end of file diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f..4ccacc5 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1,3 @@ from django.contrib import admin - -# Register your models here. +from .models import User +admin.site.register(User) diff --git a/accounts/management/commands/__init__.py b/accounts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/management/commands/superuser.py b/accounts/management/commands/superuser.py new file mode 100644 index 0000000..ecd5908 --- /dev/null +++ b/accounts/management/commands/superuser.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand +from accounts.models import User + +class Command(BaseCommand): + help = 'Creates Super User' + + def handle(self, *args, **options): + if not User.objects.filter(username='cryptoAdmin').exists(): + User.objects.create_superuser('cryptoAdmin','Admin1234') + self.stdout.write(self.style.SUCCESS(f'Successfully Created Super User')) + else: + self.stdout.write(self.style.SUCCESS(f'Admin Already Exists! No need to create new one')) + diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..5e38c70 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.8 on 2022-07-22 11:06 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('username', models.CharField(max_length=65, unique=True, verbose_name='Username')), + ('name', models.CharField(blank=True, max_length=75, null=True, verbose_name='Name')), + ('security_passphrase', models.CharField(max_length=100, verbose_name='Security Pass phrase')), + ('kc_pp', models.CharField(max_length=40, verbose_name='Kucoin Pass Phrase')), + ('kc_apikey', models.CharField(max_length=100, verbose_name='Kucoin Api Key')), + ('kc_secret', models.CharField(max_length=255, verbose_name='Kucoin Secret')), + ('last_login', models.DateTimeField(auto_now=True, verbose_name='Update at')), + ('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='Date Joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'User', + 'verbose_name_plural': 'Users', + 'ordering': ('date_joined',), + }, + ), + ] diff --git a/accounts/migrations/0002_auto_20220722_1554.py b/accounts/migrations/0002_auto_20220722_1554.py new file mode 100644 index 0000000..f49aa0a --- /dev/null +++ b/accounts/migrations/0002_auto_20220722_1554.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.8 on 2022-07-22 11:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_admin', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='user', + name='is_staff', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='user', + name='is_superuser', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/migrations/0003_remove_user_security_passphrase.py b/accounts/migrations/0003_remove_user_security_passphrase.py new file mode 100644 index 0000000..09aa595 --- /dev/null +++ b/accounts/migrations/0003_remove_user_security_passphrase.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.8 on 2022-07-23 14:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_auto_20220722_1554'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='security_passphrase', + ), + ] diff --git a/accounts/migrations/0004_auto_20220724_0016.py b/accounts/migrations/0004_auto_20220724_0016.py new file mode 100644 index 0000000..b182d5f --- /dev/null +++ b/accounts/migrations/0004_auto_20220724_0016.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.8 on 2022-07-23 19:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_remove_user_security_passphrase'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='security_passphrase', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Security Pass phrase'), + ), + migrations.AlterField( + model_name='user', + name='kc_apikey', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Kucoin Api Key'), + ), + migrations.AlterField( + model_name='user', + name='kc_pp', + field=models.CharField(blank=True, max_length=40, null=True, verbose_name='Kucoin Pass Phrase'), + ), + migrations.AlterField( + model_name='user', + name='kc_secret', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Kucoin Secret'), + ), + ] diff --git a/accounts/migrations/0005_user_auto_update_orders.py b/accounts/migrations/0005_user_auto_update_orders.py new file mode 100644 index 0000000..15496c1 --- /dev/null +++ b/accounts/migrations/0005_user_auto_update_orders.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-07-25 19:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_auto_20220724_0016'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='auto_update_orders', + field=models.BooleanField(default=True, verbose_name='Enable Auto Update Orders'), + ), + ] diff --git a/accounts/migrations/0006_auto_20220726_0002.py b/accounts/migrations/0006_auto_20220726_0002.py new file mode 100644 index 0000000..b3013e4 --- /dev/null +++ b/accounts/migrations/0006_auto_20220726_0002.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.8 on 2022-07-25 19:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_user_auto_update_orders'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='kc_apikey', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Kucoin Api Key'), + ), + migrations.AlterField( + model_name='user', + name='kc_pp', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Kucoin Pass Phrase'), + ), + migrations.AlterField( + model_name='user', + name='security_passphrase', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Security Pass phrase'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 71a8362..f4523e4 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,155 @@ +import uuid from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from rest_framework import exceptions +from rest_framework_simplejwt.tokens import RefreshToken +from Encryptor.hasher_tools import hasher +from Encryptor.encryptor import SymmetricEncryptor -# Create your models here. +from django.db import transaction + +class UserManager(BaseUserManager): + + @transaction.atomic + def create_user(self, username, security_passphrase,kc_pp,kc_apikey,kc_secret,password=None): + """ + A manager to create user to suit our needs + takes the secrets provided by user , encrypts them with a generated hash and saves them + to the database + :param username: + :param security_passphrase: + The whole idea of this field is to provide much secure app . we take the pass phrase, + hash it with custom hasher in Encryptor/hasher_tools and save the hash in the database. + + Each time user calls a request , we ask him/her to provide the security pass phrase, + we take it and check it if it's correct . then we create a kdf(key driven function) by + concatenating hashed pass phrase with hashed uuid and hashing it. + + with this kdf , we encrypt the user secrets . so each user will have his/her own individual + kdf to decrypt back the encrypted secrets and not having only one key in .env file to decrypt + every single users secrets . + :param kc_pp: + Kucoin pass phrase provided by kucoin for user + :param kc_apikey: + Kucoin api key provided by kucoin for user + :param kc_secret: + Kucoin secret phrase provided by kucoin for user + :param password: + User Password for logging i + :return: + """ + + if not username: + raise exceptions.ValidationError({'Username': ['Username is Required.']}) + try: + user = self.model( + username=username, + ) + user.save(using=self._db) + user.set_password(password) + user.is_active = True + ## we hash the provided fields , to get an encryption key (kdf) + hashed_p_phrase = hasher(security_passphrase) + hashed_uuid = hasher(str(user.id)) + encrypt_key = hasher(hashed_p_phrase + hashed_uuid) + ## instantiating SymmetricEncryptor with the generated kdf for this specific user + sym_enc = SymmetricEncryptor(encrypt_key) + + except NotImplementedError or TypeError as e: + """ + :raise NotImplementedError if salt is not provided in .env file + """ + print(e) + raise exceptions.ValidationError('Something went wrong try again!') + + # Saving hashed pass phrase in db + user.security_passphrase = hashed_p_phrase.decode("utf-8") + # Saving Encrypted secrets in the db + user.kc_pp = sym_enc.encrypt(kc_pp) + user.kc_apikey = sym_enc.encrypt(kc_apikey) + user.kc_secret = sym_enc.encrypt(kc_secret) + user.save() + return user + + def create_superuser(self, username, password=None): + if not password: + raise exceptions.ValidationError({'Password': ['Password is Required.']}) + + admin = self.model( + username=username, + + ) + + admin.set_password(password) + admin.auto_update_orders = False + admin.is_active = True + admin.is_staff = True + admin.is_superuser = True + admin.is_admin = True + admin.save(using=self._db) + return admin + + +class User(AbstractBaseUser, PermissionsMixin): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + username = models.CharField(verbose_name='Username', max_length=65, unique=True, blank=False, null=False) + name = models.CharField(verbose_name='Name', max_length=75, null=True, blank=True) + + #This field is a second security layer (each api call needs pass phrase to be provided) + security_passphrase = models.CharField(max_length=255,null=True,blank=True,verbose_name='Security Pass phrase') + + kc_pp = models.CharField(max_length=255,null=True,blank=True,verbose_name='Kucoin Pass Phrase') + kc_apikey = models.CharField(max_length=255,null=True,blank=True,verbose_name='Kucoin Api Key') + kc_secret = models.CharField(max_length=255,null=True,blank=True,verbose_name='Kucoin Secret') + auto_update_orders = models.BooleanField(default=True,verbose_name='Enable Auto Update Orders') + last_login = models.DateTimeField(verbose_name='Update at', auto_now=True) + date_joined = models.DateTimeField(verbose_name='Date Joined', auto_now_add=True) + is_admin = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + + USERNAME_FIELD = 'username' + + objects = UserManager() + + def __str__(self) -> str: + return self.username + + + def decrypted_secrets_collection(self,kdf) -> dict: + """ + Decrypts the stored kucoin secrets of the user + + :return: a dictionary containing kucoin pass_phrase , secret and api key in raw format + """ + + sym_enc = SymmetricEncryptor(kdf) + return { + 'kc_pp':sym_enc.decrypt(self.kc_pp), + 'kc_secret':sym_enc.decrypt(self.kc_secret), + 'kc_apikey':sym_enc.decrypt(self.kc_apikey), + } + + @property + def tokens(self) -> dict: + """ + Creates refresh and access token for user (simple jwt package) + :return: a dictionary containing refresh and access token + """ + token = RefreshToken.for_user(self) + data = { + 'refresh': str(token), + 'access': str(token.access_token) + } + return data + + def has_perm(self, perm, obj=None): + return self.is_superuser + + def has_module_perms(self, app_label): + return True + + class Meta: + verbose_name = 'User' + verbose_name_plural = 'Users' + ordering = ('date_joined',) \ No newline at end of file diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..94695a3 --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,60 @@ +from django.contrib.auth import authenticate +from rest_framework import serializers +from django.contrib.auth.views import get_user_model +from rest_framework.exceptions import AuthenticationFailed + +User = get_user_model() + +class RegisterSerializer(serializers.ModelSerializer): + + re_password = serializers.CharField(write_only=True,required=True) + + class Meta: + model = User + fields = ['username','re_password','password','security_passphrase','kc_secret','kc_pp','kc_apikey'] + extra_kwargs = { + 'security_passphrase': {'write_only': True,'required':True}, + 'kc_secret': {'write_only': True,'required':True}, + 'kc_pp': {'write_only': True,'required':True}, + 'kc_apikey': {'write_only': True,'required':True}, + 'password': {'write_only': True,'required':True}, + } + + def validate(self, attrs) -> dict: + """ + Checking if password and re_password are equal + If not , raised a validation error + :param attrs: + :return: + """ + if attrs.get('password') != attrs.get('re_password'): + raise serializers.ValidationError({'Password':'Passwords does not match!'}) + del attrs['re_password'] + return super().validate(attrs) + + def save(self, **kwargs) -> User: + """ + creates user with the validated data + :param kwargs: + :return: + """ + return User.objects.create_user(**self.validated_data) + + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField(write_only=True,required=True) + password = serializers.CharField(write_only=True,required=True) + + def validate(self, attrs) -> dict: + """ + authenticating the user , if it returns a user , we generate and return access and refresh token + if not , we raise an Authentication failed Error + :param attrs: + :return: + """ + user = authenticate(username=attrs.get('username'), password=attrs.get('password')) + if user: + return user.tokens + else: + raise AuthenticationFailed('Invalid credentials!') + diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..e0055a8 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import RegisterUserAPIView , LoginAPIView + +app_name = 'Accounts' + +urlpatterns = [ + path('register', RegisterUserAPIView.as_view(), name='Register'), + path('login', LoginAPIView.as_view(), name='Login') +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..595fc06 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,35 @@ -from django.shortcuts import render -# Create your views here. +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED +from rest_framework.generics import GenericAPIView, CreateAPIView +from rest_framework.permissions import AllowAny +from .serializers import RegisterSerializer, LoginSerializer + + + +class RegisterUserAPIView(CreateAPIView): + serializer_class = RegisterSerializer + permission_classes = [AllowAny] + + def create(self, request, *args, **kwargs) -> Response: + """ + Register User with the given credentials + :param request: Body is filled with 'username','re_password','password','security_passphrase','kc_secret','kc_pp','kc_apikey' fields + :param args: + :param kwargs: + :return: + """ + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response('Good to go', status=HTTP_201_CREATED) + + +class LoginAPIView(GenericAPIView): + serializer_class = LoginSerializer + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs) -> Response: + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + return Response(serializer.validated_data, status=HTTP_200_OK) diff --git a/core/serializers.py b/core/serializers.py new file mode 100644 index 0000000..1b069c1 --- /dev/null +++ b/core/serializers.py @@ -0,0 +1,51 @@ +from rest_framework import serializers +from Encryptor.hasher_tools import check_pass_phrase +from kucoin.KucoinRequestHandler import now_in_mili + + +class KucoinOrderSerializer(serializers.Serializer): + STATUS_CHOICES = (('active', 'active'), ('done', 'done')) + SIDE_CHOICES = (('buy', 'buy'), ('sell', 'sell')) + + TYPE_CHOICES = (('limit', 'limit'), ('market', 'market'), + ('limit_stop', 'limit_stop'), + ('market_stop', 'market_stop')) + + TRADE_TYPE_CHOICES = (('TRADE', 'TRADE'), ('MARGIN_TRADE ', 'MARGIN_TRADE ')) + + status = serializers.ChoiceField(required=False,write_only=True,choices=STATUS_CHOICES,default='active') + symbol = serializers.CharField(required=False, write_only=True) + side = serializers.ChoiceField(required=False, write_only=True, choices=SIDE_CHOICES) + type = serializers.ChoiceField(required=False, write_only=True, choices=TYPE_CHOICES) + tradeType = serializers.ChoiceField(required=False, write_only=True, choices=TRADE_TYPE_CHOICES) + startAt = serializers.CharField(required=False, write_only=True) + endAt = serializers.CharField(required=False, write_only=True) + + security_pass_phrase = serializers.CharField(required=True, write_only=True) + + def validate(self, attrs): + user = self.context.get('user') + if not check_pass_phrase(user,attrs.get('security_pass_phrase')): + raise serializers.ValidationError({"Security Pass Phrase":"Wrong password"}) + del attrs['security_pass_phrase'] + time_attrs = [] + + if 'startAt' not in attrs and 'endAt' not in attrs : + time_attrs = None + return attrs, time_attrs + + if 'startAt' in attrs: + time_attrs.append(int(attrs.get('startAt'))) + del attrs['startAt'] + else: + time_attrs.append(1) + + if 'endAt' in attrs: + time_attrs.append(int(attrs.get('endAt'))) + del attrs['endAt'] + else: + time_attrs.append(now_in_mili() + 10) + + + return attrs , time_attrs + diff --git a/core/tasks.py b/core/tasks.py new file mode 100644 index 0000000..3cb3969 --- /dev/null +++ b/core/tasks.py @@ -0,0 +1,15 @@ +from celery import shared_task + +from accounts.models import User +from core.updater import user_updater + +@shared_task +def update_user_orders(): + """ + once each 30(can be modified in .env as SCHEDULE) seconds , updates the orders of the users with auto update enabled + :return: + """ + users = User.objects.filter(auto_update_orders=True) + for user in users: + user_updater.delay(user.username) + return "Done" \ No newline at end of file diff --git a/core/updater.py b/core/updater.py new file mode 100644 index 0000000..1d4d68d --- /dev/null +++ b/core/updater.py @@ -0,0 +1,36 @@ +from django.conf import settings +from django.core.cache import cache +from django.core.cache.backends.base import DEFAULT_TIMEOUT +from accounts.models import User +from kucoin.KucoinRequestHandler import Kucoin +from celery import shared_task + + + +CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) + +@shared_task +def user_updater(username) -> None: + """ + fetches the active/done orders of each individual user , using the same kdf , encrypt , decrypt functionality + caches the response data with the key format like username_active/done_orders + :param username: + :return: + """ + user = User.objects.get(username=username) + try: + active_orders_obj = Kucoin(method='GET', endpoint='/api/v1/orders', sandbox=True, params={'status': 'active'}, user=user) + active_order_response = active_orders_obj.dispatcher() + done_orders_obj = Kucoin(method='GET', endpoint='/api/v1/orders', sandbox=True, params={'status': 'done'}, user=user) + done_order_response = done_orders_obj.dispatcher() + except Exception as e: + raise Exception(e) + else: + active_orders = active_order_response['data']['items'] + done_orders = done_order_response['data']['items'] + active_orders_key = f'{user.username}_active_orders' + done_orders_key = f'{user.username}_done_orders' + cache.set(active_orders_key, active_orders, timeout=CACHE_TTL) + cache.set(done_orders_key, done_orders, timeout=CACHE_TTL) + + return diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..b369d62 --- /dev/null +++ b/core/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import kucoin_orders_api_view , enable_disable_auto_update + +app_name = 'Core' + +urlpatterns = [ + path('orders', kucoin_orders_api_view, name='Orders'), + path('auto_updater', enable_disable_auto_update, name='Auto Updater'), +] diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..550625d --- /dev/null +++ b/core/utils.py @@ -0,0 +1,25 @@ +def convert(tup) -> dict: + """ + takes a tuple , generates the same as dictionary + :param tup: + :return: + """ + di = {} + for a, b in tup.items(): + di.setdefault(a, b) + return di + + +def custom_filter(data: list, filter_params: dict, time_params: list = None) -> list: + """ + + :param data: raw orders with no filter + :param filter_params: params and values in which we want to filter with , for instance : side=buy + :param time_params: a list of start and end like this [2312315,1412412] to filter by timestamp + :return: the filtered data + """ + filtered = [x for x in data if not filter_params.items() - x.items()] if filter_params else data + if time_params: + filtered = [x for x in filtered if time_params[0] <= x['createdAt'] < time_params[1]] + + return filtered diff --git a/core/views.py b/core/views.py index 91ea44a..0b8f563 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,86 @@ -from django.shortcuts import render +from django.core.cache import cache +from django.conf import settings +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK +from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import api_view, permission_classes +from django.contrib.auth.views import get_user_model +from django.core.cache.backends.base import DEFAULT_TIMEOUT +from accounts.serializers import RegisterSerializer +from core.serializers import KucoinOrderSerializer +from kucoin.KucoinRequestHandler import Kucoin -# Create your views here. +from .utils import custom_filter , convert + +CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) + +User = get_user_model() + + + +@api_view(('POST',)) +@permission_classes([IsAuthenticated]) +def kucoin_orders_api_view(request) -> Response: + """ + Everything ends up in here . the whole idea is to use redis cache to store the data provided by the kucoin api for each user + We track the open/done orders of the user , save it to the cache and filter it inside the python but not the kucoin api + the celery task updates the cached data once each 30 seconds + + the filtering provided here is just the same as kucoin using custom_filter function in utils + + if the user is asking for orders for the first time , we generate and save it to the cache + after it , we just call the key in the cache , and celery updating goes on if user has turned auto update on + :param request: + :return: + """ + data = request.data + user = request.user + serializer = KucoinOrderSerializer(data=data,context={'user':request.user}) + serializer.is_valid(raise_exception=True) + user_serializer = RegisterSerializer(user) + filter_params , time_params = serializer.validated_data + status = filter_params['status'] + key = f'{user.username}_{status}_orders' + del filter_params['status'] + if key in cache: + print('inja') + orders = cache.get(key) + else: + print('anja') + try: + kucoin_obj = Kucoin(method='GET',endpoint='/api/v1/orders',sandbox=True,params={'status':status},user=user) + resp = kucoin_obj.dispatcher() + except Exception as e: + raise Exception(e) + else: + orders = resp['data']['items'] + cache.set(key, orders, timeout=CACHE_TTL) + + if filter_params: + filter_dict = convert(filter_params) + orders = custom_filter(orders, filter_dict,time_params) + result = { + 'user': user_serializer.data, + 'orders': orders + } + return Response(result, status=HTTP_200_OK) + + +@api_view(('POST',)) +@permission_classes([IsAuthenticated]) +def enable_disable_auto_update(request) -> Response: + """ + An endpoint to enable disable auto update whenever it is called + + if auto update field is true , by calling this endpoint it gets disabled + :param request: + :return: + """ + user = request.user + if user.auto_update_orders: + user.auto_update_orders = False + user.save() + return Response({'Order Auto Update':'Disabled'}, status=HTTP_200_OK) + user.auto_update_orders = True + user.save() + return Response({'Order Auto Update': 'Enabled'}, status=HTTP_200_OK) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5980f20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,84 @@ +version: "3.7" + + +services: + + + redis_service: + image: redis:alpine + container_name: djang_redis + ports: + - "6380:6379" + + networks: + forum_net: + ipv4_address: 192.167.0.2 + + + db_service: + image: postgres + volumes: + - ./db:/var/lib/postgresql/data + + environment: + - POSTGRES_DB=crypto_reader + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + container_name: django_db_service + + ports: + - "5434:5432" + + + networks: + forum_net: + ipv4_address: 192.167.0.3 + + + + django_app: + build: + context: . + dockerfile: Dockerfile + volumes: + - .:/crypto_reader + - static:/crypto_reader/static + - media:/crypto_reader/media + ports: + - "8000:8000" + container_name: Djservice + + + depends_on: + - db_service + networks: + forum_net: + ipv4_address: 192.167.0.6 + restart: on-failure + + proxy: + build: ./nginx + ports: + - "80:80" + + volumes: + - static:/static + - media:/media + networks: + forum_net: + ipv4_address: 192.167.0.7 + + depends_on: + - django_app + restart: on-failure + +networks: + forum_net: + ipam: + driver: default + config: + - subnet: 192.167.0.0/24 + +volumes: + static: + media: \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..22d776b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +python manage.py makemigrations --noinput +python manage.py migrate --noinput +python manage.py collectstatic --noinput +python manage.py superuser +supervisord -c supervisord.conf + + + + + + + + + diff --git a/kucoin/KucoinRequestHandler.py b/kucoin/KucoinRequestHandler.py new file mode 100644 index 0000000..738df89 --- /dev/null +++ b/kucoin/KucoinRequestHandler.py @@ -0,0 +1,137 @@ +import base64 +import hashlib +import hmac +import time +import urllib.parse +from abc import ABC, abstractmethod +import requests + +from Encryptor.hasher_tools import hasher + + +def now_in_mili() -> int: + """ + :return: timestamp of now + """ + return int(time.time() * 1000) + + +class AbstractKucoin(ABC): + + @abstractmethod + def header_maker(self): + pass + + @abstractmethod + def secret_revealed(self): + pass + + @abstractmethod + def url_maker(self): + pass + + @abstractmethod + def signature_generator(self): + pass + + @abstractmethod + def passphrase_generator(self): + pass + + @abstractmethod + def dispatcher(self): + pass + + +class Kucoin(AbstractKucoin): + user_secrets = None + + def __init__(self, user, endpoint: str, method: str, params: dict = None, sandbox: bool = False): + """ + + :param user: a user instance to get values like passphrase and uuid for decrypting secrets + :param endpoint: used for calling the endpoint of kucoin apis + :param method: The method we want to use to send request . also needed for creating api sign + :param params: take any accepted parameter by the kucoin endpoint and sends with the request + :param sandbox: to get the main url + """ + self.user = user + self.endpoint = endpoint + self.params = params + self.method = method + self.sandbox_mode = sandbox + self.secret_revealed() + + def secret_revealed(self) -> dict: + """ + uses the 'decrypted_secrets_collection' method of User model to decrypt api key , secret key and pass phrase + as mentioned before we hash uuid , concatenate it to hashed passphrase and hash it to generate our kdf in which we can decrypt with it + + this method is called upon instantiation , to set value for user_secrets variable + :return: + """ + hashed_uuid = hasher(str(self.user.id)) + decryption_kdf = hasher(self.user.security_passphrase + hashed_uuid.decode('utf-8')) + secrets = self.user.decrypted_secrets_collection(decryption_kdf) + self.user_secrets = secrets + return secrets + + def url_maker(self) -> str: + """ + provides the url we need to send request + :return: + """ + if self.sandbox_mode: + return "https://openapi-sandbox.kucoin.com" + self.endpoint + return "https://api.kucoin.com" + self.endpoint + + def signature_generator(self) -> bytes: + """ + according to the kucoin docs , we need to generate a signature using endpoint,method,timestamp of now and params + and hashing with encrypted secret key + we add it to the request headers later as "KC-API-SIGN" + :return: + """ + api_secret = self.user_secrets.get('kc_secret') + str_to_sign = str(now_in_mili()) + self.method + self.endpoint + '?' + urllib.parse.urlencode(self.params) + signature = base64.b64encode( + hmac.new(api_secret.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256).digest()) + return signature + + def passphrase_generator(self) -> bytes: + """ + use the encrypted secret key and api pass phrase and generate a hashed passphrase + we add it to the request headers later as "KC-API-PASSPHRASE" + :return: + """ + api_secret = self.user_secrets.get('kc_secret') + api_passphrase = self.user_secrets.get('kc_pp') + passphrase = base64.b64encode(hmac.new(api_secret.encode('utf-8'), api_passphrase.encode('utf-8'), hashlib.sha256).digest()) + return passphrase + + def header_maker(self) -> dict: + """ + according to the docs of kucoin , this whole header is needed to be sent with request to authenticate + :return: + """ + return { + "KC-API-SIGN": self.signature_generator(), + "KC-API-TIMESTAMP": str(now_in_mili()), + "KC-API-KEY": self.user_secrets.get('kc_apikey'), + "KC-API-PASSPHRASE": self.passphrase_generator(), + "KC-API-KEY-VERSION": "2" + } + + def dispatcher(self) -> dict: + """ + we dispatch the request with the generated credentials + + we use method variable to determine the method + generate url by calling url_maker method + add headers and params that we generated above + :return: + """ + response = requests.request(self.method, self.url_maker(), headers=self.header_maker(), params=self.params) + if response.status_code != 200: + raise Exception({'Error': response.json()}) + return response.json() diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..164bd38 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:1.19.0-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d + diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..09c3c0b --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,21 @@ +upstream web { + server 192.167.0.6:8000; +} + +server { + + listen 80; + + location / { + proxy_pass http://web; + } + location /static/ { + alias /static/; + } + + location /media/ { + alias /media/; + + } + +} diff --git a/requirements.txt b/requirements.txt index e69de29..28b223e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,15 @@ +Django==3.2.8 +django-cors-headers +djangorestframework +djangorestframework-simplejwt +gunicorn +Markdown +python-decouple +redis>=3.5 +celery>=5.0 +django-celery-beat +supervisor +psycopg2-binary +cryptography>=3.3.1 +django-redis +drf-yasg \ No newline at end of file diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..7cf0bc9 --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,24 @@ +[supervisord] +logfile=/crypto_reader/supervisor/supervisord.log ; supervisord log file +logfile_maxbytes=50MB ; maximum size of logfile before rotation +logfile_backups=10 ; number of backed up logfiles +loglevel=debug ; info, debug, warn, trace +pidfile=/crypto_reader/supervisor/supervisord.pid ; pidfile location +nodaemon=true ; run supervisord as a daemon +minfds=1024 ; number of startup file descriptors +minprocs=200 ; number of process descriptors +user=root ; defaults to whichever user is runs supervisor +childlogdir=/crypto_reader/supervisor ; where child log files will live + + + + + +[program:gunicorn] +command=gunicorn DjangoCryptoReader.wsgi:application --bind 0.0.0.0:8000 + +[program:celery] +command=celery -A DjangoCryptoReader worker --loglevel=info + +[program:celery_beat] +command=celery -A DjangoCryptoReader beat --loglevel=info \ No newline at end of file From 07fcaf1c28b81ea4630ca77fb035c08f90dec182 Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Wed, 27 Jul 2022 17:20:18 +0430 Subject: [PATCH 3/7] fixed entrypoint.sh file --- entrypoint.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 22d776b..11d89b6 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,9 +1,10 @@ #!/bin/sh -python manage.py makemigrations --noinput -python manage.py migrate --noinput -python manage.py collectstatic --noinput +python manage.py makemigrations --no-input +python manage.py migrate --no-input +python manage.py collectstatic --no-input python manage.py superuser +mkdir -p supervisor supervisord -c supervisord.conf From eac51653deb649edd408bb571613a5485ed69cd5 Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Wed, 27 Jul 2022 17:31:54 +0430 Subject: [PATCH 4/7] fixed entrypoint.sh file --- entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 11d89b6..b311baa 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/sh -python manage.py makemigrations --no-input -python manage.py migrate --no-input -python manage.py collectstatic --no-input +python manage.py makemigrations --noinput +python manage.py migrate --noinput +python manage.py collectstatic --noinput python manage.py superuser mkdir -p supervisor supervisord -c supervisord.conf From b47aaabef8beccd09841e577878d577014a8f3cd Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Wed, 27 Jul 2022 17:38:42 +0430 Subject: [PATCH 5/7] fixing entrypoint.sh file --- entrypoint.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index b311baa..6b12e7a 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,11 +1,11 @@ #!/bin/sh -python manage.py makemigrations --noinput -python manage.py migrate --noinput -python manage.py collectstatic --noinput -python manage.py superuser -mkdir -p supervisor -supervisord -c supervisord.conf +python manage.py makemigrations --noinput && python manage.py migrate --noinput && python manage.py collectstatic --noinput && python manage.py superuser && mkdir -p supervisor && supervisord -c supervisord.conf + + + + + From 6b35f131527e77e780e37b15876a9dfb983bac7c Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Wed, 27 Jul 2022 17:47:41 +0430 Subject: [PATCH 6/7] fixing entrypoint.sh file --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 6b12e7a..d95a738 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh -python manage.py makemigrations --noinput && python manage.py migrate --noinput && python manage.py collectstatic --noinput && python manage.py superuser && mkdir -p supervisor && supervisord -c supervisord.conf +python manage.py makemigrations --noinput && python manage.py migrate --noinput && python manage.py collectstatic --noinput && python manage.py superuser && mkdir -p supervisor && supervisord -c supervisord.conf && python manage.py collectstatic From 287653fe145df972ae733666c8a4dbbe8fcc40aa Mon Sep 17 00:00:00 2001 From: atabak-hooshangi Date: Sat, 3 Feb 2024 15:58:23 +0100 Subject: [PATCH 7/7] remove prints --- core/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/views.py b/core/views.py index 0b8f563..35ad748 100644 --- a/core/views.py +++ b/core/views.py @@ -43,10 +43,8 @@ def kucoin_orders_api_view(request) -> Response: key = f'{user.username}_{status}_orders' del filter_params['status'] if key in cache: - print('inja') orders = cache.get(key) else: - print('anja') try: kucoin_obj = Kucoin(method='GET',endpoint='/api/v1/orders',sandbox=True,params={'status':status},user=user) resp = kucoin_obj.dispatcher()