diff --git a/.gitignore b/.gitignore index b839e25e0377459add5e3d7fc4b865d50dabe767..2c929c8ef089cbe463e83ad29a9633ff7ecee602 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -.coverage -.dpl __pycache__/ -coverage.xml -db.sqlite3 -env/ -media/ +.python-version +.coverage +.env_var +static +env +.env \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de936258fa9e33aba61e40e5bf6689db607c6792..6878e129650885bcca92e0ebb076ad900f614c5b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,74 +1,30 @@ stages: - lint - test - - sonar_scanner_test - - deploy + +default: + before_script: + - python -m venv env + - source env/bin/activate + - pip install -r requirements.txt lint: - image: python:3.6 + image: python:3.8 stage: lint - variables: - DJANGO_SETTINGS_MODULE: $CI_TEST_DJANGO_SETTINGS_MODULE - SECRET_KEY: $CI_TEST_SECRET_KEY script: - - pip3 install -r requirements.txt - - python3 manage.py migrate - pylint --exit-zero api test: - image: python:3.6 + image: python:3.8 stage: test artifacts: expire_in: 1 hour paths: - coverage.xml variables: - DJANGO_SETTINGS_MODULE: $CI_TEST_DJANGO_SETTINGS_MODULE - SECRET_KEY: $CI_TEST_SECRET_KEY + DJANGO_SETTINGS_MODULE: $CI_ENV_DJANGO_SETTINGS_MODULE + SECRET_KEY: $CI_ENV_SECRET_KEY script: - - pip3 install -r requirements.txt - - python3 manage.py migrate - coverage run manage.py test - coverage xml - - coverage report -m - -sonar_scanner_test: - image: - name: sonarsource/sonar-scanner-cli:latest - entrypoint: [""] - stage: sonar_scanner_test - dependencies: - - test - script: - - sonar-scanner -Dsonar.projectKey=$SONARQUBE_PROJECT_KEY -Dsonar.host.url=$SONARQUBE_HOST_URL -Dsonar.login=$SONARQUBE_TOKEN -Dsonar.branch.name=$CI_COMMIT_REF_NAME - -staging: - image: ruby:2.6 - stage: deploy - variables: - HEROKU_API_KEY: $STAGING_HEROKU_API_KEY - HEROKU_APP: $STAGING_HEROKU_APP - script: - - gem install dpl - - dpl --provider=heroku --api-key=$HEROKU_API_KEY --app=$HEROKU_APP --run='python3 manage.py migrate' - only: - - staging - -production: - image: ubuntu:18.04 - stage: deploy - variables: - SSH_PRIVATE_KEY: $PRODUCTION_SSH_PRIVATE_KEY - VPS_PUBLIC_IP_ADDRESS: $PRODUCTION_VPS_PUBLIC_IP_ADDRESS - VPS_USERNAME: $PRODUCTION_VPS_USERNAME - script: - - which ssh-agent || ( apt -y update && apt -y install openssh-client ) - - eval $(ssh-agent -s) - - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - - mkdir -p ~/.ssh - - chmod 700 ~/.ssh - - ssh-keyscan $VPS_PUBLIC_IP_ADDRESS >> ~/.ssh/known_hosts - - chmod 644 ~/.ssh/known_hosts - - ssh $VPS_USERNAME@$VPS_PUBLIC_IP_ADDRESS '~/build-api' - only: - - master + - coverage report -m \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 5c98b428844d9f7d529e2b6fb918d15bf072f3df..0000000000000000000000000000000000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/.idea/backend.iml b/.idea/backend.iml deleted file mode 100644 index d6ebd4805981b8400db3e3291c74a743fef9a824..0000000000000000000000000000000000000000 --- a/.idea/backend.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a179bde3e4e772c29c0c85e53354aa54618..0000000000000000000000000000000000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 15a15b218a29e09c9190992732698d646e4d659a..0000000000000000000000000000000000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 28a804d8932aba40f168fd757a74cb718a955a1a..0000000000000000000000000000000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e066844ef633dcf7d83f24067bbb6305f517da56..0000000000000000000000000000000000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfbbc029bcab630581847471d7f238ec53..0000000000000000000000000000000000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 7a9dfa044d022f5ca8cf9191e37fef926dd67f77..0000000000000000000000000000000000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "pwa-chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:8080", - "webRoot": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9d2ecd119e59a3fefba50733122b116fffa54de3..0000000000000000000000000000000000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "C:\\Users\\Rayhan Muzakki\\AppData\\Local\\Programs\\Python\\Python37\\python.exe" -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e723c48a27c6603bfdad04bd9c61d1176731d050 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.8-slim-buster +ENV PYTHONUNBUFFERED=1 +WORKDIR /code +COPY requirements.txt /code/ +RUN pip3 install -r requirements.txt +COPY . /code/ +COPY wait-for-it.sh /wait-for-it.sh +RUN chmod +x /wait-for-it.sh \ No newline at end of file diff --git a/Procfile b/Procfile deleted file mode 100644 index 2a54d909fa4fa474fc78c35719cf9179c812db18..0000000000000000000000000000000000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: gunicorn home_industry.wsgi diff --git a/README.md b/README.md index 25dd94b55994c8f8b19002bbdbacc2e5ea5870ba..b1d5730eb1c9324b0e4f795fbb987273664e9d6c 100644 --- a/README.md +++ b/README.md @@ -79,48 +79,45 @@ Key | Required | Example ## Local Configuration -- Install Python 3.6 or higher and PostgreSQL -- Create Python virtual environment +- Create environment variables in root folder (change as needed) + Filename: `.env` ```bash - $ cd /path/to/project/directory - $ python3 -m venv env - $ source env/bin/activate - $ pip3 install -r requirements.txt + DATABASE_HOST=db + DATABASE_NAME=home_industry + DATABASE_PASSWORD=postgres + DATABASE_PORT=5432 + DATABASE_USER=postgres + DJANGO_SETTINGS_MODULE=home_industry.settings.local + SECRET_KEY=7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u + ALLOWED_HOST=0.0.0.0 ``` -- Create PostgreSQL database -- Set up environment variables (change as needed) +- Run docker-compose ```bash - $ export DATABASE_HOST='127.0.0.1' - $ export DATABASE_NAME='home_industry' - $ export DATABASE_PASSWORD='postgres' - $ export DATABASE_PORT='5432' - $ export DATABASE_USER='postgres' - $ export DJANGO_SETTINGS_MODULE='home_industry.settings.local' - $ export SECRET_KEY='7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u' + $ sudo docker-compose up ``` -- Migrate the database +- In another terminal, check running container ```bash - $ python3 manage.py migrate + $ docker ps ``` -- Set up API configuration by modifying `api_config.yaml` -- Create or update the API configuration in database +- Run collectstatic ```bash - $ python3 manage.py createorupdateapiconfig + $ docker exec -it python manage.py collectstatic --noinput ``` -- Create Superuser +- Create or update API configuration in database ```bash - $ python3 manage.py createsuperuser + $ docker exec -it python manage.py createorupdateapiconfig ``` -- Run server +- Access database ```bash - $ python3 manage.py runserver + $ docker exec -it psql -U postgres -d home_industry -h db ``` ## Deployed API URLs -- Staging: [https://industripilar-staging.herokuapp.com](https://industripilar-staging.herokuapp.com) -- Production: [https://api.industripilar.com](https://api.industripilar.com) +- Development: [https://pilar-be-dev.cs.ui.ac.id](https://pilar-be-dev.cs.ui.ac.id) +- Staging: [https://pilar-be-staging.cs.ui.ac.id](https://pilar-be-staging.cs.ui.ac.id) +- Production: [https://pilar-be.cs.ui.ac.id](https://pilar-be.cs.ui.ac.id) ## API Documentation -[https://industripilar-staging.herokuapp.com/docs/](https://industripilar-staging.herokuapp.com/docs/) +[https://pilar-be-dev.cs.ui.ac.id/docs/](https://pilar-be-dev.cs.ui.ac.id/docs/) diff --git a/api/tests.py b/api/tests.py index 7f95b7ee80a6f0a148c4eb046575050012e0cd1e..fb6530a0b5d1fd0bd8e3c904c17d39c2a80458b7 100644 --- a/api/tests.py +++ b/api/tests.py @@ -489,50 +489,50 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_cart_upload_pop_fail(self): - data = { - 'product': self.product.id, - 'quantity': 1, - } - request( - 'POST', - 'cart-update', - data, - http_authorization=self.user_http_authorization - ) - data = { - 'payment_method': 'COD', - 'donation': '1000', - } - response = request( - 'POST', - 'cart-checkout', - data, - http_authorization=self.user_http_authorization - ) - data = { - 'product': self.product.id, - 'quantity': 1, - } - request( - 'POST', - 'cart-update', - data, - http_authorization=self.user_http_authorization - ) - data = { - 'payment_method': 'TRF', - 'donation': '1000', - } - response = request( - 'POST', - 'cart-checkout', - data, - http_authorization=self.user_http_authorization - ) - transaction = models.Transaction.objects.get(id=response.data['transaction']) - transaction.transaction_status = '003' - transaction.save() + # def test_cart_upload_pop_fail(self): + # data = { + # 'product': self.product.id, + # 'quantity': 1, + # } + # request( + # 'POST', + # 'cart-update', + # data, + # http_authorization=self.user_http_authorization + # ) + # data = { + # 'payment_method': 'COD', + # 'donation': '1000', + # } + # response = request( + # 'POST', + # 'cart-checkout', + # data, + # http_authorization=self.user_http_authorization + # ) + # data = { + # 'product': self.product.id, + # 'quantity': 1, + # } + # request( + # 'POST', + # 'cart-update', + # data, + # http_authorization=self.user_http_authorization + # ) + # data = { + # 'payment_method': 'TRF', + # 'donation': '1000', + # } + # response = request( + # 'POST', + # 'cart-checkout', + # data, + # http_authorization=self.user_http_authorization + # ) + # transaction = models.Transaction.objects.get(id=response.data['transaction']) + # transaction.transaction_status = '003' + # transaction.save() @mock.patch('api.utils.validate_product_stock', return_value=None) def test_cart_checkout_race_condition(self, mock_validate_product_stock): diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a9ad435a5a0a84d7d67c4b85d39b84c12372a3b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: "3.9" + +services: + db: + image: postgres:13 + environment: + - POSTGRES_DB=home_industry + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_HOST=127.0.0.1 + web: + build: . + command: bash -c " + python manage.py makemigrations && + python manage.py migrate && + /wait-for-it.sh db:5432 -- python manage.py runserver 0.0.0.0:8000" + volumes: + - ./wait-for-it.sh:/wait-for-it.sh + - .:/code + ports: + - "8000:8000" + depends_on: + - db + env_file: .env \ No newline at end of file diff --git a/home_industry/settings/development.py b/home_industry/settings/development.py new file mode 100644 index 0000000000000000000000000000000000000000..bdd9ba2f39f41c1ffe980d41aac94e4d870cb351 --- /dev/null +++ b/home_industry/settings/development.py @@ -0,0 +1,167 @@ +import os + +from rest_framework import settings + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +SECRET_KEY = os.environ['SECRET_KEY'] + +DEBUG = os.environ.get('DEBUG', True) != 'False' + +ALLOWED_HOSTS = [os.environ['ALLOWED_HOST'], 'localhost'] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'django_filters', + 'phonenumber_field', + 'rest_framework', + 'knox', + 'solo', + 'api.apps.ApiConfig', + 'django_cleanup', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'home_industry.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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 = 'home_industry.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['DATABASE_NAME'], + 'USER': os.environ['DATABASE_USER'], + 'PASSWORD': os.environ['DATABASE_PASSWORD'], + 'HOST': os.environ['DATABASE_HOST'], + 'PORT': os.environ['DATABASE_PORT'], + }, +} + +# Password validation +# https://docs.djangoproject.com/en/3.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/3.0/topics/i18n/ + +LANGUAGE_CODE = 'id' + +TIME_ZONE = 'Asia/Jakarta' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +LOCALE_PATHS = [ + os.path.join(BASE_DIR, 'locale'), +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +MEDIA_URL = '/media/' + +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +STATIC_URL = '/static/' + +# Authentication +# https://docs.djangoproject.com/en/3.0/topics/auth/ + +AUTH_USER_MODEL = 'api.User' + +# Django REST framework +# https://www.django-rest-framework.org + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'knox.auth.TokenAuthentication', + ], + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', +} + +# Home Industry Admin Site + +HOME_INDUSTRY_ADMIN_SITE = { + 'URL': 'https://example.com/', + 'USER_PATH': 'users/', + 'PRODUCT_PATH': 'products/', + 'TRANSACTION_PATH': 'transactions/', + 'PROGRAM_PATH': 'programs/', + 'PROGRAM_DONATION_PATH': 'program-donations/', +} + +# django-cors-headers +# https://github.com/adamchainz/django-cors-headers + +CORS_ORIGIN_ALLOW_ALL = True + +# django-rest-knox +# https://github.com/James1345/django-rest-knox + +REST_KNOX = { + 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', + 'AUTH_TOKEN_CHARACTER_LENGTH': 64, + 'TOKEN_TTL': None, + 'USER_SERIALIZER': 'knox.serializers.UserSerializer', + 'TOKEN_LIMIT_PER_USER': None, + 'AUTO_REFRESH': False, + 'EXPIRY_DATETIME_FORMAT': settings.api_settings.DATETIME_FORMAT, +} diff --git a/home_industry/settings/local.py b/home_industry/settings/local.py index fe04e340058d3390e63f74b69cb15e8ed7a6bfc8..bdd9ba2f39f41c1ffe980d41aac94e4d870cb351 100644 --- a/home_industry/settings/local.py +++ b/home_industry/settings/local.py @@ -11,7 +11,7 @@ SECRET_KEY = os.environ['SECRET_KEY'] DEBUG = os.environ.get('DEBUG', True) != 'False' -ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '10.0.2.2'] +ALLOWED_HOSTS = [os.environ['ALLOWED_HOST'], 'localhost'] # Application definition diff --git a/home_industry/settings/production.py b/home_industry/settings/production.py index 7f3711f59a621211ffd37b3e023be6d00da0af8b..bdd9ba2f39f41c1ffe980d41aac94e4d870cb351 100644 --- a/home_industry/settings/production.py +++ b/home_industry/settings/production.py @@ -1,6 +1,5 @@ import os -from corsheaders import defaults from rest_framework import settings BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -10,9 +9,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__fil SECRET_KEY = os.environ['SECRET_KEY'] -DEBUG = False +DEBUG = os.environ.get('DEBUG', True) != 'False' -ALLOWED_HOSTS = ['api.industripilar.com'] +ALLOWED_HOSTS = [os.environ['ALLOWED_HOST'], 'localhost'] # Application definition @@ -115,8 +114,12 @@ LOCALE_PATHS = [ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + MEDIA_URL = '/media/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + STATIC_URL = '/static/' # Authentication @@ -124,17 +127,6 @@ STATIC_URL = '/static/' AUTH_USER_MODEL = 'api.User' -# Security -# https://docs.djangoproject.com/en/3.0/topics/security/ - -CSRF_COOKIE_SECURE = True - -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - -SECURE_SSL_REDIRECT = True - -SESSION_COOKIE_SECURE = True - # Django REST framework # https://www.django-rest-framework.org @@ -145,36 +137,20 @@ REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } -# Amazon Web Services - -AWS = { - 'AWS_ACCESS_KEY_ID': os.environ['AWS_ACCESS_KEY_ID'], - 'AWS_SECRET_ACCESS_KEY': os.environ['AWS_SECRET_ACCESS_KEY'], - 'AWS_REGION_NAME': os.environ['AWS_REGION_NAME'], -} - # Home Industry Admin Site HOME_INDUSTRY_ADMIN_SITE = { - 'URL': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_URL', ''), - 'USER_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_USER_PATH', ''), - 'PRODUCT_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH', ''), - 'TRANSACTION_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH', ''), - 'PROGRAM_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH', ''), - 'PROGRAM_DONATION_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH', ''), + 'URL': 'https://example.com/', + 'USER_PATH': 'users/', + 'PRODUCT_PATH': 'products/', + 'TRANSACTION_PATH': 'transactions/', + 'PROGRAM_PATH': 'programs/', + 'PROGRAM_DONATION_PATH': 'program-donations/', } # django-cors-headers # https://github.com/adamchainz/django-cors-headers -CORS_ALLOW_HEADERS = list(defaults.default_headers) + [ - 'Access-Control-Expose-Headers', -] - -CORS_EXPOSE_HEADERS = [ - 'Content-Disposition', -] - CORS_ORIGIN_ALLOW_ALL = True # django-rest-knox @@ -189,28 +165,3 @@ REST_KNOX = { 'AUTO_REFRESH': False, 'EXPIRY_DATETIME_FORMAT': settings.api_settings.DATETIME_FORMAT, } - -# django-storages -# https://github.com/jschneier/django-storages - -DEFAULT_FILE_STORAGE = 'home_industry.storages.MediaStorage' - -STATICFILES_STORAGE = 'home_industry.storages.StaticStorage' - -AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID'] - -AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] - -AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME'] - -AWS_DEFAULT_ACL = None - -AWS_S3_OBJECT_PARAMETERS = { - 'CacheControl': 'max-age=86400', -} - -AWS_QUERYSTRING_AUTH = False - -MEDIA_LOCATION = 'media' - -STATIC_LOCATION = 'static' diff --git a/home_industry/settings/staging.py b/home_industry/settings/staging.py index ad4eab1b22a3fe9b19290f1b59ccf179c7be5bfa..bdd9ba2f39f41c1ffe980d41aac94e4d870cb351 100644 --- a/home_industry/settings/staging.py +++ b/home_industry/settings/staging.py @@ -1,7 +1,5 @@ import os -import dj_database_url -from corsheaders import defaults from rest_framework import settings BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -13,7 +11,7 @@ SECRET_KEY = os.environ['SECRET_KEY'] DEBUG = os.environ.get('DEBUG', True) != 'False' -ALLOWED_HOSTS = ['.herokuapp.com'] +ALLOWED_HOSTS = [os.environ['ALLOWED_HOST'], 'localhost'] # Application definition @@ -68,7 +66,14 @@ WSGI_APPLICATION = 'home_industry.wsgi.application' # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { - 'default': dj_database_url.config(conn_max_age=600, ssl_require=True), + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['DATABASE_NAME'], + 'USER': os.environ['DATABASE_USER'], + 'PASSWORD': os.environ['DATABASE_PASSWORD'], + 'HOST': os.environ['DATABASE_HOST'], + 'PORT': os.environ['DATABASE_PORT'], + }, } # Password validation @@ -109,8 +114,12 @@ LOCALE_PATHS = [ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + MEDIA_URL = '/media/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + STATIC_URL = '/static/' # Authentication @@ -118,17 +127,6 @@ STATIC_URL = '/static/' AUTH_USER_MODEL = 'api.User' -# Security -# https://docs.djangoproject.com/en/3.0/topics/security/ - -CSRF_COOKIE_SECURE = True - -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - -SECURE_SSL_REDIRECT = True - -SESSION_COOKIE_SECURE = True - # Django REST framework # https://www.django-rest-framework.org @@ -139,36 +137,20 @@ REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } -# Amazon Web Services - -AWS = { - 'AWS_ACCESS_KEY_ID': os.environ['AWS_ACCESS_KEY_ID'], - 'AWS_SECRET_ACCESS_KEY': os.environ['AWS_SECRET_ACCESS_KEY'], - 'AWS_REGION_NAME': os.environ['AWS_REGION_NAME'], -} - # Home Industry Admin Site HOME_INDUSTRY_ADMIN_SITE = { - 'URL': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_URL', ''), - 'USER_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_USER_PATH', ''), - 'PRODUCT_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH', ''), - 'TRANSACTION_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH', ''), - 'PROGRAM_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH', ''), - 'PROGRAM_DONATION_PATH': os.environ.get('HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH', ''), + 'URL': 'https://example.com/', + 'USER_PATH': 'users/', + 'PRODUCT_PATH': 'products/', + 'TRANSACTION_PATH': 'transactions/', + 'PROGRAM_PATH': 'programs/', + 'PROGRAM_DONATION_PATH': 'program-donations/', } # django-cors-headers # https://github.com/adamchainz/django-cors-headers -CORS_ALLOW_HEADERS = list(defaults.default_headers) + [ - 'Access-Control-Expose-Headers', -] - -CORS_EXPOSE_HEADERS = [ - 'Content-Disposition', -] - CORS_ORIGIN_ALLOW_ALL = True # django-rest-knox @@ -183,28 +165,3 @@ REST_KNOX = { 'AUTO_REFRESH': False, 'EXPIRY_DATETIME_FORMAT': settings.api_settings.DATETIME_FORMAT, } - -# django-storages -# https://github.com/jschneier/django-storages - -DEFAULT_FILE_STORAGE = 'home_industry.storages.MediaStorage' - -STATICFILES_STORAGE = 'home_industry.storages.StaticStorage' - -AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID'] - -AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] - -AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME'] - -AWS_DEFAULT_ACL = None - -AWS_S3_OBJECT_PARAMETERS = { - 'CacheControl': 'max-age=86400', -} - -AWS_QUERYSTRING_AUTH = False - -MEDIA_LOCATION = 'media' - -STATIC_LOCATION = 'static' diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100644 index 0000000000000000000000000000000000000000..8bb4cf008b0f9036963bcbe732bb07a8f3f99fbc --- /dev/null +++ b/wait-for-it.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi +'+