diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b35096 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# migrations +*/migrations/* +!*/migrations/__init__.py + +# PyCache +*/__pycache__/* +*/*/__pycache__/* + +# database +*.sqlite3 + +# venv +*env +*Pipfile +*Pipfile.lock + +# IDE +*.vscode +*.idea + +# uploads media +media/* +!media/default/ + +# static files +staticfiles/* diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/asgi.py b/core/asgi.py new file mode 100644 index 0000000..5b80ece --- /dev/null +++ b/core/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.development') + +application = get_asgi_application() diff --git a/core/settings/common.py b/core/settings/common.py new file mode 100644 index 0000000..cec8319 --- /dev/null +++ b/core/settings/common.py @@ -0,0 +1,141 @@ +import os +from pathlib import Path + +# jazzmin imported settings configuration +from utils.jazzmin_settings import jazzmin_settings + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + +# Application definition +INSTALLED_APPS = [ + # Admin Panel + 'jazzmin', + # Main App + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Internal App + + # External App + 'debug_toolbar', + 'axes', +] + +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', + # Third-Party Middleware + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', + 'axes.middleware.AxesMiddleware', + 'django_session_timeout.middleware.SessionTimeoutMiddleware', +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + +# Password validation +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 +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +STATIC_URL = '/static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# media +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Axes Configuration Settings +AUTHENTICATION_BACKENDS = [ + # AxesStandaloneBackend should be the first backend in the AUTHENTICATION_BACKENDS list. + 'axes.backends.AxesStandaloneBackend', + + # Django ModelBackend is the default authentication backend. + 'django.contrib.auth.backends.ModelBackend', +] + +AXES_FAILURE_LIMIT: 3 # how many times a user can fail a login +AXES_COOLOFF_TIME: 2 # Wait 2 hours before attempting to login again +AXES_RESET_ON_SUCCESS = True +# AXES_LOCKOUT_TEMPLATE = 'account-locked.html' --> if need -> enable + +# Jazzmin settings configuration +JAZZMIN_SETTINGS = jazzmin_settings + +# Session setting configuration +SESSION_EXPIRE_SECONDS = 604800 # 1 week -> Expire +SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True +SESSION_TIMEOUT_REDIRECT = '/admin/' + +# Caches setting configuration +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-cache-key-for-chat-request-limit', + 'TIMEOUT': 60 * 60 * 24, # Cache timeout set to 24 hours (1 day) + } +} + +# email settings configuration +EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND') +EMAIL_HOST = os.environ.get('EMAIL_HOST') +EMAIL_PORT = os.environ.get('EMAIL_PORT') +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') +EMAIL_USE_TLS = True +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL') + +# ُTimeOut system +TIMEOUT = 300 diff --git a/core/settings/development.py b/core/settings/development.py new file mode 100644 index 0000000..f1418ec --- /dev/null +++ b/core/settings/development.py @@ -0,0 +1,22 @@ +from .common import * + + +# Default secret key in debug mode +SECRET_KEY = 'll1*tq$z$%t7-$x@8*ow+*xn-av!swn!aux@)gs!c*jx=1&h64' + +# Debug mode +DEBUG = True + + +# Debug Toolbar +INTERNAL_IPS = [ + '127.0.0.1', +] + +# Default Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} diff --git a/core/settings/production.py b/core/settings/production.py new file mode 100644 index 0000000..ca1779d --- /dev/null +++ b/core/settings/production.py @@ -0,0 +1,50 @@ +import os + +from .common import * + +import dj_database_url + +from dotenv import load_dotenv + +# Loading environment variable's +load_dotenv() + +# Django secret key +SECRET_KEY = os.environ.get('SECRET_KEY') + +# Enable sever mode +DEBUG = False + +# Allowed run server in this host +FIRST_HOST = os.environ.get('FIRST_HOST') +SECOND_HOST = os.environ.get('SECOND_HOST') + +ALLOWED_HOSTS = ['127.0.0.1', FIRST_HOST, SECOND_HOST] + +# Final database +DATABASES = { + 'default': dj_database_url.config(default=os.environ.get('DATABASE_URL')), +} + + +# Configure Cache system +CACHE_MIDDLEWARE_ALIAS = 'default' +CACHE_MIDDLEWARE_SECONDS = 604800 # 7 Days +CACHE_MIDDLEWARE_KEY_PREFIX = '' + +# CSRF Attack +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# XSS Attack +SECURE_BROWSER_XSS_FILTER = True +SECURE_COUNT_TYPE_NOSNIFF = True + +# CORS Origin Header settings configuration +CORS_ORIGIN = os.environ.get('CORS_ORIGIN') +CSRF_ORIGINS = os.environ.get('CSRF_ORIGINS') +CORS_ALL_ORIGINS = os.environ.get('CORS_ALLOW_ALL_ORIGINS') +CORS_ALLOW_CREDENTIALS = os.environ.get('CORS_ALLOW_CREDENTIALS') +CORS_ALLOWED_ORIGINS = [CORS_ORIGIN] +CSRF_TRUSTED_ORIGINS = [CSRF_ORIGINS] +CORS_ALLOW_ALL_ORIGINS = CORS_ALL_ORIGINS diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..22d89cd --- /dev/null +++ b/core/urls.py @@ -0,0 +1,33 @@ +import os + +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +import debug_toolbar +from dotenv import load_dotenv + +# Loading environment variable's +load_dotenv() + +if settings.DEBUG: + ADMIN_DIRECTORY = os.environ.setdefault('ADMIN_DIRECTORY', 'admin') +else: + ADMIN_DIRECTORY = os.environ.get('ADMIN_DIRECTORY') + +urlpatterns = [ + path(f'{ADMIN_DIRECTORY}/', admin.site.urls), +] + +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + +# Media static +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Debug toolbar +if settings.DEBUG: + urlpatterns = [ + path('__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/core/wsgi.py b/core/wsgi.py new file mode 100644 index 0000000..3b180fa --- /dev/null +++ b/core/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.development') + +application = get_wsgi_application() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5cbf55d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.9" + +services: + postgres_db: + image: postgres:latest + container_name: postgres_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data/ + + pgadmin: + image: dpage/pgadmin4:latest + container_name: pgadmin + depends_on: + - postgres_db + ports: + - "5051:5050" + + dj_backend: + build: . + ports: + - "8000:8000" + command: bash -c "python manage.py migrate --noinput && python manage.py collectstatic --noinput && exec gunicorn core.wsgi:application -b 0.0.0.0:8000 -w 4" + volumes: + - .:/app/ + depends_on: + - postgres_db + environment: + - DJANGO_SETTINGS_MODULE=core.settings.production + - DEBUG=False + + nginx: + image: nginx:latest + container_name: nginx + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - dj_backend + +volumes: + postgres_data: diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..da92cc7 --- /dev/null +++ b/dockerfile @@ -0,0 +1,17 @@ +# Pull base image +From python:3.10.4-slim-bullseye + +# Set envirement variable +ENV PIP_DISABLE_PIP_VERSION_CHECK 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set work directory +WORKDIR /app/ShadStore + +# Install dependencies +COPY ./requirements.txt . +RUN pip install -r requirements.txt + +# Copy project +COPY . . diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..e42f7d4 --- /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', 'core.settings.development') + 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/nginx.conf b/nginx.conf new file mode 100644 index 0000000..e000cc1 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://dj_backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c57b6bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +django==4.2.3 +django-session-timeout==0.1.0 +django-debug-toolbar==4.1.0 +django-jazzmin==2.6.0 +django-axes==6.0.5 +dj-database-url==2.0.0 +psycopg2-binary==2.9.6 +gunicorn==20.1.0 +Pillow==9.5.0 +pip-chill==1.0.3 +python-dotenv==1.0.0 +whitenoise==6.5.0 diff --git a/static/img/logo.png b/static/img/logo.png new file mode 100644 index 0000000..374c1c1 Binary files /dev/null and b/static/img/logo.png differ diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..e69de29 diff --git a/static/style/main.css b/static/style/main.css new file mode 100644 index 0000000..e69de29 diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..e69de29 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/jazzmin_settings.py b/utils/jazzmin_settings.py new file mode 100644 index 0000000..3c0740d --- /dev/null +++ b/utils/jazzmin_settings.py @@ -0,0 +1,139 @@ +# Jazzmin Settings Configuration +jazzmin_settings = { + # title of the window (Will default to current_admin_site.site_title if absent or None) + 'site_title': 'behshadrhp panel management', + + # Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None) + 'site_header': 'BehshadRHP', + + # Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None) + 'site_brand': 'BehshadRHP', + + # Logo to use for your site, must be present in static files, used for brand on top left + 'site_logo': 'img/logo.png', + + # Logo to use for your site, must be present in static files, used for login form logo (defaults to site_logo) + 'login_logo': None, + + # Logo to use for login form in dark themes (defaults to login_logo) + 'login_logo_dark': None, + + # CSS classes that are applied to the logo above + 'site_logo_classes': 'img-circle', + + # Relative path to a favicon for your site, will default to site_logo if absent (ideally 32x32 px) + 'site_icon': None, + + # Welcome text on the login screen + 'welcome_sign': 'Hi There, Welcome Back', + + # Copyright on the footer + 'copyright': 'behshadrhp', + + # List of model admins to search from the search bar, search bar omitted if excluded + # If you want to use a single search field you dont need to use a list, you can use a simple string + 'search_model': ['auth.User', 'auth.Group'], + + # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user + 'user_avatar': None, + + ############ + # Top Menu # + ############ + + # Links to put along the top menu + 'topmenu_links': [ + + # Url that gets reversed (Permissions can be added) + {'name': 'managment', 'url': 'admin:index', 'permissions': ['auth.view_user']}, + + # external url that opens in a new window (Permissions can be added) + {'name': 'support', 'url': '', 'new_window': True}, + + # model admin to link to (Permissions checked against model) + {'model': 'auth.User'}, + + # App with dropdown menu to all its models pages (Permissions checked against models) + {'app': 'books'}, + ], + + ############# + # User Menu # + ############# + + # Additional links to include in the user menu on the top right ('app' url type is not allowed) + 'usermenu_links': [ + {'name': 'support', 'url': '', 'new_window': True}, + {'model': 'auth.user'} + ], + + ############# + # Side Menu # + ############# + + # Whether to display the side menu + 'show_sidebar': True, + + # Whether to aut expand the menu + 'navigation_expanded': True, + + # Hide these apps when generating side menu e.g (auth) + 'hide_apps': [], + + # Hide these models when generating side menu (e.g auth.user) + 'hide_models': [], + + # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models) + 'order_with_respect_to': ['auth', 'books', 'books.author', 'books.book'], + + # Custom links to append to app groups, keyed on app name + 'custom_links': { + 'books': [{ + 'name': 'Make Messages', + 'url': 'make_messages', + 'icon': 'fas fa-comments', + 'permissions': ['books.view_book'] + }] + }, + + # Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.0.2,5.0.3,5.0.4,5.0.5,5.0.6,5.0.7,5.0.8,5.0.9,5.1.0,5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,5.11.2,5.11.1,5.10.0,5.9.0,5.8.2,5.8.1,5.7.2,5.7.1,5.7.0,5.6.3,5.5.0,5.4.2 + # for the full list of 5.13.0 free icon classes + 'icons': { + 'auth': 'fas fa-users-cog', + 'auth.user': 'fas fa-user', + 'auth.Group': 'fas fa-users', + }, + # Icons that are used when one is not manually specified + 'default_icon_parents': 'fas fa-chevron-circle-right', + 'default_icon_children': 'fas fa-circle', + + ################# + # Related Modal # + ################# + # Use modals instead of popups + 'related_modal_active': False, + + ############# + # UI Tweaks # + ############# + # Relative paths to custom CSS/JS scripts (must be present in static files) + 'custom_css': None, + 'custom_js': None, + # Whether to link font from fonts.googleapis.com (use custom_css to supply font otherwise) + 'use_google_fonts_cdn': True, + # Whether to show the UI customizer on the sidebar + 'show_ui_builder': False, + + ############### + # Change view # + ############### + # Render out the change view as a single form, or in tabs, current options are + # - single + # - horizontal_tabs (default) + # - vertical_tabs + # - collapsible + # - carousel + 'changeform_format': 'horizontal_tabs', + # override change forms on a per modeladmin basis + 'changeform_format_overrides': {'auth.user': 'collapsible', 'auth.group': 'vertical_tabs'}, +}