diff --git a/.gitignore b/.gitignore index 4f97b84..794c055 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,18 @@ -/config/* -/static/admin -/application/**/migrations/* -/archive/* -/logs/* -/packages/* -/profilepictures/* -/temp -/tmp +/data/* +/data/logs/* +/data/tls/* +/data/static/* +!/data/logs/ +!/data/logs/.gitkeep +!/data/tls/ +!/data/tls/.gitkeep +!/data/static/ +!/data/static/.gitkeep +!/data/Caddyfile +!/data/*.example.* + +/venv + __pycache__ .vscode *.pem -!/config/config.sample.sh -!/config/Caddyfile -!/config/tls/ -!/profilepictures/default.svg -!.gitkeep diff --git a/README.md b/README.md index 27df4cf..c30c6f9 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,44 @@ This (exaggeration intended) most incredible piece of software is written in Pyt HTML, CSS, JS, Bash and uses Django and PostgreSQL. You have to bring your own PostgreSQL Database though. +# Getting started -## Setup, Installation, Updating and Dependencies +## System Requirements -You can find the latest releases [here](https://gitlab.com/W13R/drinks-manager/-/releases), but you should consider using Git to easily switch between versions. -For more information see [Setup](docs/Setup.md). +Beneath a `PostgreSQL` DBMS, you need the following things: +- `pg_config` (Ubuntu: `libpq-dev`, RHEL: `libpq-devel`) +- `Caddy` 2.4.3+ (HTTP Reverse Proxy & Static File Server) +- `gcc`, `gettext` +- `Python` 3.9+ + - `venv` + - `pip` +- `Python` header files (RHEL: `python3-devel`, Ubuntu: `python3-dev`) -## Configuration +## Create Environment & Install dependencies + +Run the following from the main directory: + +``` +./scripts/setup-env.sh +``` + +## Activate Venv + +**On every new session**, before running commands with manage.py or developing, you have to activate the venv: + +``` +source ./venv/bin/activate +``` + +If you see `(venv)` before your command prompt, it worked! + +## + +# Configuration see [Configuration](docs/Configuration.md) +# Usage -## Usage - -After setup, run ```./run.sh help``` to see a help text. -Start the production server with ```./run.sh server```. You can ignore the error message about the "lifespan error". -For more commands, see [Commands](docs/Commands.md). +... diff --git a/application/app/__init__.py b/app/__init__.py similarity index 100% rename from application/app/__init__.py rename to app/__init__.py diff --git a/application/app/admin.py b/app/admin.py similarity index 100% rename from application/app/admin.py rename to app/admin.py index 4234240..5cab141 100644 --- a/application/app/admin.py +++ b/app/admin.py @@ -15,6 +15,7 @@ from .forms import CustomDrinkForm from .forms import CustomGlobalForm from .forms import CustomRegisterTransactionForm + # Admin Site class CustomAdminSite(admin.AdminSite): @@ -100,7 +101,6 @@ class CustomRegisterAdmin(admin.ModelAdmin): self.message_user(request, f"Revoked {queryset.count()} supplies.") delete_selected_new.short_description = "Revoke selected transactions" - adminSite.register(Register, CustomRegisterAdmin) diff --git a/app/apps.py b/app/apps.py new file mode 100644 index 0000000..bcfe39b --- /dev/null +++ b/app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "app" diff --git a/application/app/context_processors.py b/app/context_processors.py similarity index 100% rename from application/app/context_processors.py rename to app/context_processors.py diff --git a/application/app/forms.py b/app/forms.py similarity index 100% rename from application/app/forms.py rename to app/forms.py diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py new file mode 100644 index 0000000..71ead05 --- /dev/null +++ b/app/migrations/0001_initial.py @@ -0,0 +1,267 @@ +# Generated by Django 4.1.6 on 2023-02-11 15:24 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "balance", + models.DecimalField(decimal_places=2, default=0.0, max_digits=8), + ), + ( + "allow_order_with_negative_balance", + models.BooleanField(default=False), + ), + ( + "profile_picture_filename", + models.CharField(default="default.svg", max_length=25), + ), + ("allowed_to_supply", models.BooleanField(default=False)), + ( + "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", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="Drink", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("product_name", models.CharField(max_length=64)), + ( + "content_litres", + models.DecimalField(decimal_places=3, default=0.5, max_digits=6), + ), + ( + "price", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ("available", models.PositiveIntegerField(default=0)), + ("deleted", models.BooleanField(default=False)), + ("do_not_count", models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name="Global", + fields=[ + ( + "name", + models.CharField( + max_length=42, primary_key=True, serialize=False, unique=True + ), + ), + ("comment", models.TextField()), + ("value_float", models.FloatField(default=0.0)), + ("value_string", models.TextField()), + ], + ), + migrations.CreateModel( + name="RegisterTransaction", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "transaction_sum", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ( + "old_transaction_sum", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ("datetime", models.DateTimeField(default=django.utils.timezone.now)), + ("is_user_deposit", models.BooleanField(default=False)), + ("comment", models.TextField(default=" ")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "transaction", + "verbose_name_plural": "transactions", + }, + ), + migrations.CreateModel( + name="Order", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("datetime", models.DateTimeField(default=django.utils.timezone.now)), + ("amount", models.PositiveIntegerField(default=1, editable=False)), + ("product_name", models.CharField(editable=False, max_length=64)), + ( + "price_sum", + models.DecimalField( + decimal_places=2, default=0, editable=False, max_digits=6 + ), + ), + ( + "content_litres", + models.DecimalField( + decimal_places=3, default=0, editable=False, max_digits=6 + ), + ), + ( + "drink", + models.ForeignKey( + limit_choices_to=models.Q(("available__gt", 0)), + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="app.drink", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/application/drinks_manager/__init__.py b/app/migrations/__init__.py similarity index 100% rename from application/drinks_manager/__init__.py rename to app/migrations/__init__.py diff --git a/application/app/models.py b/app/models.py similarity index 97% rename from application/app/models.py rename to app/models.py index a3bb11c..3f4f3d1 100644 --- a/application/app/models.py +++ b/app/models.py @@ -2,7 +2,6 @@ from django.db import models from django.conf import settings from django.contrib.auth.models import AbstractUser -from django_currentuser.db.models import CurrentUserField from django.forms import ValidationError from django.utils import timezone @@ -62,7 +61,7 @@ class RegisterTransaction(models.Model): datetime = models.DateTimeField(default=timezone.now) is_user_deposit = models.BooleanField(default=False) comment = models.TextField(default=" ") - user = CurrentUserField() + user = models.ForeignKey(User, on_delete=models.CASCADE) def save(self, *args, **kwargs): if self._state.adding: @@ -100,7 +99,7 @@ class Order(models.Model): null=True, limit_choices_to=models.Q(available__gt=0) # Query only those drinks with a availability greater than (gt) 0 ) - user = CurrentUserField() + user = models.ForeignKey(User, on_delete=models.CASCADE) datetime = models.DateTimeField(default=timezone.now) amount = models.PositiveIntegerField(default=1, editable=False) diff --git a/application/app/sql_queries.py b/app/sql_queries.py similarity index 100% rename from application/app/sql_queries.py rename to app/sql_queries.py diff --git a/static/css/appform.css b/app/static/css/appform.css similarity index 100% rename from static/css/appform.css rename to app/static/css/appform.css diff --git a/static/css/custom_number_input.css b/app/static/css/custom_number_input.css similarity index 100% rename from static/css/custom_number_input.css rename to app/static/css/custom_number_input.css diff --git a/static/css/history.css b/app/static/css/history.css similarity index 100% rename from static/css/history.css rename to app/static/css/history.css diff --git a/static/css/index.css b/app/static/css/index.css similarity index 100% rename from static/css/index.css rename to app/static/css/index.css diff --git a/static/css/login.css b/app/static/css/login.css similarity index 100% rename from static/css/login.css rename to app/static/css/login.css diff --git a/static/css/main.css b/app/static/css/main.css similarity index 100% rename from static/css/main.css rename to app/static/css/main.css diff --git a/static/css/statistics.css b/app/static/css/statistics.css similarity index 100% rename from static/css/statistics.css rename to app/static/css/statistics.css diff --git a/misc/icons/favicon.ico b/app/static/favicon.ico similarity index 100% rename from misc/icons/favicon.ico rename to app/static/favicon.ico diff --git a/misc/icons/favicon.png b/app/static/favicon.png similarity index 100% rename from misc/icons/favicon.png rename to app/static/favicon.png diff --git a/static/js/autoreload.js b/app/static/js/autoreload.js similarity index 100% rename from static/js/autoreload.js rename to app/static/js/autoreload.js diff --git a/static/js/custom_number_input.js b/app/static/js/custom_number_input.js similarity index 100% rename from static/js/custom_number_input.js rename to app/static/js/custom_number_input.js diff --git a/static/js/deposit.js b/app/static/js/deposit.js similarity index 100% rename from static/js/deposit.js rename to app/static/js/deposit.js diff --git a/static/js/logged_out.js b/app/static/js/logged_out.js similarity index 100% rename from static/js/logged_out.js rename to app/static/js/logged_out.js diff --git a/static/js/login.js b/app/static/js/login.js similarity index 100% rename from static/js/login.js rename to app/static/js/login.js diff --git a/static/js/main.js b/app/static/js/main.js similarity index 100% rename from static/js/main.js rename to app/static/js/main.js diff --git a/static/js/order.js b/app/static/js/order.js similarity index 100% rename from static/js/order.js rename to app/static/js/order.js diff --git a/static/js/supply.js b/app/static/js/supply.js similarity index 100% rename from static/js/supply.js rename to app/static/js/supply.js diff --git a/application/app/templates/admin/base_site.html b/app/templates/admin/base_site.html similarity index 100% rename from application/app/templates/admin/base_site.html rename to app/templates/admin/base_site.html diff --git a/application/app/templates/admin/index.html b/app/templates/admin/index.html similarity index 100% rename from application/app/templates/admin/index.html rename to app/templates/admin/index.html diff --git a/application/app/templates/baselayout.html b/app/templates/baselayout.html similarity index 100% rename from application/app/templates/baselayout.html rename to app/templates/baselayout.html diff --git a/application/app/templates/deposit.html b/app/templates/deposit.html similarity index 100% rename from application/app/templates/deposit.html rename to app/templates/deposit.html diff --git a/application/app/templates/footer.html b/app/templates/footer.html similarity index 100% rename from application/app/templates/footer.html rename to app/templates/footer.html diff --git a/application/app/templates/globalmessage.html b/app/templates/globalmessage.html similarity index 100% rename from application/app/templates/globalmessage.html rename to app/templates/globalmessage.html diff --git a/application/app/templates/history.html b/app/templates/history.html similarity index 100% rename from application/app/templates/history.html rename to app/templates/history.html diff --git a/application/app/templates/index.html b/app/templates/index.html similarity index 100% rename from application/app/templates/index.html rename to app/templates/index.html diff --git a/application/app/templates/order.html b/app/templates/order.html similarity index 100% rename from application/app/templates/order.html rename to app/templates/order.html diff --git a/application/app/templates/registration/logged_out.html b/app/templates/registration/logged_out.html similarity index 100% rename from application/app/templates/registration/logged_out.html rename to app/templates/registration/logged_out.html diff --git a/application/app/templates/registration/login.html b/app/templates/registration/login.html similarity index 100% rename from application/app/templates/registration/login.html rename to app/templates/registration/login.html diff --git a/application/app/templates/statistics.html b/app/templates/statistics.html similarity index 100% rename from application/app/templates/statistics.html rename to app/templates/statistics.html diff --git a/application/app/templates/supply.html b/app/templates/supply.html similarity index 100% rename from application/app/templates/supply.html rename to app/templates/supply.html diff --git a/application/app/templates/userpanel.html b/app/templates/userpanel.html similarity index 100% rename from application/app/templates/userpanel.html rename to app/templates/userpanel.html diff --git a/application/app/urls.py b/app/urls.py similarity index 91% rename from application/app/urls.py rename to app/urls.py index 70feadd..f6f8cc4 100644 --- a/application/app/urls.py +++ b/app/urls.py @@ -16,10 +16,8 @@ urlpatterns = [ path('accounts/password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'), path('accounts/password_change_done/', views.redirect_home, name='password_change_done'), path('admin/', adminSite.urls), - # custom-handled resources - path('profilepictures', views.profile_pictures), # API # path('api/order-drink', views.api_order_drink), path('api/deposit', views.api_deposit), path('api/supply', views.api_supply) -] \ No newline at end of file +] diff --git a/application/app/views.py b/app/views.py similarity index 89% rename from application/app/views.py rename to app/views.py index 33e4b49..abb0023 100644 --- a/application/app/views.py +++ b/app/views.py @@ -15,7 +15,6 @@ from django.http.response import HttpResponse from django.shortcuts import render from django.utils.translation import gettext as _ - from django.utils.formats import decimal from . import sql_queries @@ -24,12 +23,6 @@ from .models import Drink from .models import Order from .models import RegisterTransaction -# - -profile_pictures_path = Path(settings.PROFILE_PICTURES).resolve() - -# login view - def login_page(request): @@ -67,8 +60,6 @@ def login_page(request): }) -# actual application - @login_required def index(request): context = { @@ -76,6 +67,7 @@ def index(request): } return render(request, "index.html", context) + @login_required def history(request): context = { @@ -83,6 +75,7 @@ def history(request): } return render(request, "history.html", context) + @login_required def order(request, drinkid): try: @@ -94,10 +87,12 @@ def order(request, drinkid): except Drink.DoesNotExist: return HttpResponseRedirect("/") + @login_required def deposit(request): return render(request, "deposit.html", {}) + @login_required def statistics(request): context = { @@ -110,57 +105,34 @@ def statistics(request): } return render(request, "statistics.html", context) + @login_required def supply(request): return render(request, "supply.html") + @login_required def redirect_home(request): return HttpResponseRedirect("/") -# Custom-Handled Resources - -def profile_pictures(request): - if not "name" in request.GET: - return HttpResponse(b"", status=400) - ppic_filepath = Path(profile_pictures_path / request.GET["name"]).resolve() - try: - ppic_filepath.relative_to(profile_pictures_path) - except: - return HttpResponse("No.", status=403) - if ppic_filepath.is_file(): - return FileResponse(ppic_filepath.open('rb')) - else: - return FileResponse(b"", status=404) - - # API for XHR requests # @login_required def api_order_drink(request): - # check request -> make order - user = request.user - try: - if user.allow_order_with_negative_balance or user.balance > 0: - drinkid = int(request.POST["drinkid"]) amount = int(request.POST["numberofdrinks"]) - drink = Drink.objects.get(pk=drinkid) - if ((drink.do_not_count and drink.available > 0) or (drink.available >= amount)) and not drink.deleted: Order.objects.create(drink=drink, user=user, amount=amount) return HttpResponse("success", status=200) else: return HttpResponse("notAvailable", status=400) - else: raise Exception("Unexpected input or missing privileges.") - except Exception as e: print(f"An exception occured while processing an order: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) @@ -168,15 +140,10 @@ def api_order_drink(request): @login_required def api_deposit(request): - # check request -> deposit - user = request.user - try: - amount = decimal.Decimal(request.POST["depositamount"]) - if 0.00 < amount < 9999.99: # create transaction RegisterTransaction.objects.create( @@ -185,26 +152,19 @@ def api_deposit(request): is_user_deposit=True, user=user ) - # return HttpResponse("success", status=200) else: raise Exception("Deposit amount too big or small.") - except Exception as e: print(f"An exception occured while processing a transaction: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) @login_required def api_supply(request): - # check request -> supply - user = request.user - try: - price = decimal.Decimal(request.POST["supplyprice"]) description = str(request.POST["supplydescription"]) - if 0.00 < price < 9999.99 and (user.allowed_to_supply or user.is_superuser): # create transaction RegisterTransaction.objects.create( @@ -213,10 +173,8 @@ def api_supply(request): is_user_deposit=False, user=user ) - # return HttpResponse("success", status=200) else: raise Exception("Unexpected input or missing privileges.") - except Exception as e: print(f"An exception occured while processing a supply transaction: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) diff --git a/application/app/apps.py b/application/app/apps.py deleted file mode 100644 index e61ab8c..0000000 --- a/application/app/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.contrib.admin.apps import AdminConfig - - -class DAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'app' diff --git a/application/app/middleware.py b/application/app/middleware.py deleted file mode 100644 index 43acf6c..0000000 --- a/application/app/middleware.py +++ /dev/null @@ -1,11 +0,0 @@ - -# Define CSP middleware: - -def csp_middleware(get_response): - - def middleware(request): - response = get_response(request) - response["content-security-policy"] = "default-src 'self'" - return response - - return middleware diff --git a/application/app/tests.py b/application/app/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/application/app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/application/drinks_manager/settings.py b/application/drinks_manager/settings.py deleted file mode 100644 index d01503f..0000000 --- a/application/drinks_manager/settings.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -Django settings for drinks_manager project. - -Generated by 'django-admin startproject' using Django 3.2.5. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - -import os - -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/3.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key secret! -django_secret_key_absolute_fp = os.environ["DJANGO_SK_ABS_FP"] -with open(django_secret_key_absolute_fp) as secret_key_file: - SECRET_KEY = secret_key_file.read().strip() - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = (os.environ["DJANGO_DEBUG"].lower() == "true") - - -ALLOWED_HOSTS = [ - "*" -] - - -### ----------------- ### - - -# Application definition - -INSTALLED_APPS = [ - "app.apps.DAppConfig", - '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.locale.LocaleMiddleware", - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - "django_currentuser.middleware.ThreadLocalUserMiddleware", - "app.middleware.csp_middleware" -] - -ROOT_URLCONF = 'drinks_manager.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', - "app.context_processors.app_version" - ], - }, - }, -] - -WSGI_APPLICATION = 'drinks_manager.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': os.environ["PGDB_DB"], - 'USER': os.environ["PGDB_USER"], - 'PASSWORD': os.environ["PGDB_PASSWORD"], - 'HOST': os.environ["PGDB_HOST"], - 'PORT': str(os.environ["PGDB_PORT"]) - } -} - -CONN_MAX_AGE = 20 # keep database connections alive for n seconds - - -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - -if os.environ["DJANGO_ENABLE_PASSWORD_VALIDATION"].lower() == "true": - 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', - }, - ] -else: - AUTH_PASSWORD_VALIDATORS = [] - - -AUTH_USER_MODEL = "app.User" - -# user will be logged out after x seconds -SESSION_COOKIE_AGE = int(os.environ["DJANGO_SESSION_COOKIE_AGE"]) - - -# more security settings - -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_SECURE = True - - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ - -LANGUAGE_CODE = os.environ["DJANGO_LANGUAGE_CODE"] # this is the default and fallback language (currently only de-de and en-us supported) - -TIME_ZONE = os.environ["DJANGO_TIME_ZONE"] - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -LOCALE_PATHS = [ - BASE_DIR / "locale" -] - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.environ["STATIC_FILES"] - - -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# - -APP_VERSION = os.environ["APP_VERSION"] - -try: - CURRENCY_SUFFIX = os.environ["CURRENCY_SUFFIX"] -except KeyError: - CURRENCY_SUFFIX = "$" - -PROFILE_PICTURES = os.environ["PROFILE_PICTURES"] diff --git a/config/config.sample.sh b/config/config.sample.sh deleted file mode 100644 index 51aa8f0..0000000 --- a/config/config.sample.sh +++ /dev/null @@ -1,31 +0,0 @@ -# environment variables - -export HTTP_PORT=80 # required by caddy, will be redirected to https -export HTTPS_PORT=443 # actual port for the webinterface - -export DJANGO_PORT=8001 # caddy's http port (should be blocked by the firewall) - -export DJANGO_SESSION_COOKIE_AGE=600 # auto-logout, in seconds -export SESSION_CLEAR_INTERVAL=120 # interval for automatic session clearing, in minutes - -export DJANGO_LANGUAGE_CODE="en" # the default and fallback language, currently only de and en are supported. -export DJANGO_TIME_ZONE="CET" # your timezone - -export CURRENCY_SUFFIX="$" # if you have another currency symbol, you can specify it here - -# Do you want to enable password validation? -# (numeric PINs as Password will not be seen as valid) -export DJANGO_ENABLE_PASSWORD_VALIDATION="true" - -# database connection (postgresql) -export PGDB_DB="" # The name of the database -export PGDB_USER="" # The database user -export PGDB_PASSWORD='' # The password for the database user -export PGDB_HOST="127.0.0.1" # The hostname of your database -export PGDB_PORT=5432 # The port your database is listening on - -# log files -# only change if you know what you are doing -export CADDY_ACCESS_LOG="$(pwd)/logs/http-access.log" -export CADDY_LOG="$(pwd)/logs/caddy.log" -export APPLICATION_LOG="$(pwd)/logs/application.log" diff --git a/config/Caddyfile b/data/Caddyfile similarity index 56% rename from config/Caddyfile rename to data/Caddyfile index e5fe817..94b7a54 100644 --- a/config/Caddyfile +++ b/data/Caddyfile @@ -1,29 +1,36 @@ { - # disable admin backend + # disable unwanted stuff admin off + skip_install_trust # define the ports by the environment variables http_port {$HTTP_PORT} https_port {$HTTPS_PORT} } -https:// { +0.0.0.0 { # the tls certificates - tls ./config/tls/server.pem ./config/tls/server-key.pem + tls {$DATADIR}/tls/server.pem {$DATADIR}/tls/server-key.pem route { + # profile pictures + file_server /profilepictures/* { + root {$DATADIR}/profilepictures/.. + } # static files file_server /static/* { - root {$STATIC_FILES}/.. + root {$DATADIR}/static/.. } # favicon redir /favicon.ico /static/favicon.ico # reverse proxy to the (django) application - reverse_proxy localhost:{$DJANGO_PORT} + reverse_proxy localhost:{$APPLICATION_PORT} + # set additional security headers + header Content-Security-Policy "default-src 'self'" } # use compression encode gzip # logging log { - output file {$CADDY_ACCESS_LOG} + output file {$ACCESS_LOG} format filter { wrap console fields { diff --git a/data/config.example.yml b/data/config.example.yml new file mode 100644 index 0000000..c24a2c8 --- /dev/null +++ b/data/config.example.yml @@ -0,0 +1,37 @@ +--- +app: + # The secret key, used for security protections + # This MUST be a secret, very long, random string + secret_key: "!!!insert random data!!!" + # The port for the asgi application + # This should be blocked by the firewall + application_port: 8001 + # Used for auto-logout, in seconds + session_cookie_age: 600 + # Interval for automatic session clearing, in minutes + session_clear_interval: 120 + # The default and fallback language, currently only de and en are supported. + language_code: "en" + # Your timezone + timezone: "CET" + # Specify the suffix for your currency + currency_suffix: "$" + # Enable/Disable password validation + # (numeric PINs are NOT valid when this is set to true) + password_validation: true +db: + # Database configuration + database: "drinks" + user: "drinks" + password: "insert password" + host: "127.0.0.1" + port: 5432 +caddy: + # Ports that the web server listens on + http_port: 80 + https_port: 443 +logs: + # Logfile paths + caddy: "./data/logs/caddy.log" + http_access: "./data/logs/http-access.log" + application: "./data/logs/application.log" diff --git a/archive/.gitkeep b/data/logs/.gitkeep similarity index 100% rename from archive/.gitkeep rename to data/logs/.gitkeep diff --git a/config/tls/.gitkeep b/data/tls/.gitkeep similarity index 100% rename from config/tls/.gitkeep rename to data/tls/.gitkeep diff --git a/docs/Commands.md b/docs/Commands.md deleted file mode 100644 index b47bbc3..0000000 --- a/docs/Commands.md +++ /dev/null @@ -1,81 +0,0 @@ -# Commands - -You run a command with - -``` -./run.sh -``` - -## Available Commands - ---- - -### `server` -This starts the application (a caddy instance, uvicorn with the Django application and a scheduler that automatically removes expired session data). -Log files will be written. - ---- - -### `setup` -This sets up some database tables, views, and more, generates a secret key for the application and lets you create an admin user. - ---- - -### `create-admin` -Lets you create an admin user - ---- - -### `generate-secret-key` -Generate a new random secret key for Django. -This will overwrite the old one. -Warning: After running this, current sessions will be invalid, and the users have to relogin. Don't run this command while the server is running. - ---- - -### `clear-sessions` -manually remove all expired sessions from the database - ---- - -### `force-db-upgrade` -force a database migration and -upgrade. -This is mainly used in development. - ---- - -### `archive-tables` -archive (copy & delete) all entries in app_order and app_registertransaction. -Use this to archive old orders or transactions (e.g. when the database gets too big). - ---- - -### `development-server` -This starts a caddy instance, the Django development server with DEBUGGING enabled and the session-clear-scheduler. -Only the HTTP-Access-Log will be written to its logfile, other logs will be written to the console. - ---- - -### `shell` - -Start a Django shell. - ---- - -### `help` -Show a help text - ---- - - -## Examples - -Run the production server: -``` -./run.sh server -``` - -Create a new admin: -``` -./run.sh create-admin -``` \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md deleted file mode 100644 index 00d5933..0000000 --- a/docs/Configuration.md +++ /dev/null @@ -1,14 +0,0 @@ -# Configuration - -## Main Configuration - -`config/config.sh` - -There is a sample configuration with explanations: [/config/config.sample.sh](/config/config.sample.sh) - - -## Caddy (Reverse Proxy & Static File Server) - -[config/Caddyfile](/config/Caddyfile) - -The default configuration should work out of the box, don't edit this file unless you know what you're doing. diff --git a/docs/Setup.md b/docs/Setup.md deleted file mode 100644 index bf3d0c4..0000000 --- a/docs/Setup.md +++ /dev/null @@ -1,110 +0,0 @@ -# Setup - -## I. Dependencies - -Before the actual setup, you have to satisfy the following dependencies: - - -### System - -- `pg_config` - - Ubuntu: `libpq-dev` - - Fedora/RHEL: `libpq-devel` -- `Caddy` 2.4.3+ (HTTP Reverse Proxy & Static File Server) -- `gcc`, `gettext` -- `Python` 3.9+ with pip - - `Python` header files - - Fedora/RHEL: `python3-devel` - - Ubuntu: `python3-dev` - - -### Python Packages (pip) - -All required python packages are listed in [requirements.txt](/requirements.txt) - -You can install the required python packages with -```bash -./install-pip-dependencies.sh -``` - -## II.A Installation - -You can get the latest version with git: - -``` -git clone --branch release-x https://gitlab.com/W13R/drinks-manager.git -``` -(replace x with the latest version) - -Alternatively, you can download the [latest release](https://gitlab.com/W13R/drinks-manager/-/releases) and extract the files to your prefered destination. - -**Warning:** - -Make shure that you set the correct file permissions, especially for the config files !! - -The following should be sufficient: - -```bash -chmod -R u+rw,g+r,g-w,o-rwx -``` - - -## II.B Update - -If you installed the application with git, you can run the following in the drinks-manager directory to update to the new version: - -``` -git fetch -git checkout x -``` -(replace x with the new version) - -If you downloaded the application from the releases page, you can download the new release in the same manner, and overwrite the old files with the new ones. - -You have to restart the application server to apply the changes. -WARNING: The auto-upgrade mechanism may expect you to input information. Therefore, you should start the application from the command-line the first time after an update. - -Further upgrading-instructions may be provided in the Release Notes on the Releases Page of this Project (Deployments -> Releases). - - -## III. Database - -This project is using PostgreSQL. You have to set up a database: - -```sql -create user drinksmanager password ''; -create database drinksmgr owner drinksmanager; -``` - -After creating the user, you have to edit your `pg_hba.conf` (see https://www.postgresql.org/docs/current/auth-pg-hba-conf.html). -Add the following line: -``` -host drinksmgr drinksmanager 127.0.0.1/32 md5 -``` - -Now you can configure your database connection in `config/config.sh`. - - -## IV. HTTPS & TLS Certificates - -A TLS/SSL certificate and key is required. -Filepaths: - -- `config/tls/server.pem` for the certificate -- `config/tls/server-key.pem` for the key - -You can set another filepath for those files in your caddy configuration at [config/Caddyfile](/config/Caddyfile). - - -## V. Configuration - -see [Configuration](Configuration.md) - - -## VI. Run Setup Command - -run `./run.sh setup` - -This will automatically set up database tables, views and entries, set up Django and let you create a admin user. - -After this, start the server with `./run.sh server` and navigate to `https://your.ip.add.ress:port/admin/`. diff --git a/install-pip-dependencies.sh b/install-pip-dependencies.sh deleted file mode 100755 index 6c02848..0000000 --- a/install-pip-dependencies.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# install the required python packages - -wd=$(dirname $0) - -pip3 install -r "$wd/requirements.txt" -t "$wd/packages" diff --git a/lib/activate-devel-env.sh b/lib/activate-devel-env.sh deleted file mode 100755 index 3467721..0000000 --- a/lib/activate-devel-env.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source ./lib/env.sh -source ./config/config.sh -export DJANGO_DEBUG=true -export PYTHONPATH="./packages" diff --git a/lib/archive-tables.py b/lib/archive-tables.py deleted file mode 100644 index a1901cc..0000000 --- a/lib/archive-tables.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import os, sys - -from datetime import datetime -from pathlib import Path - -from psycopg2 import connect - - -# archive (copy & delete) all entries in app_order and app_registertransaction - -timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") -archive_folder = Path("./archive") -orders_archive_path = archive_folder / ("orders-archive-" + timestamp + ".csv") -transactions_archive_path = archive_folder / ("transactions-archive-" + timestamp + ".csv") - - -if __name__ == "__main__": - exit_code = 1 - connection = connect( - user = os.environ["PGDB_USER"], - password = os.environ["PGDB_PASSWORD"], - host = os.environ["PGDB_HOST"], - port = os.environ["PGDB_PORT"], - database = os.environ["PGDB_DB"] - ) - cur = connection.cursor() - try: - print(f"Starting archiving to {orders_archive_path.__str__()} and {transactions_archive_path.__str__()}...") - # # # # # - # copy - with orders_archive_path.open("w") as of: - cur.copy_expert( - "copy (select * from app_order) to STDOUT with csv delimiter ';'", - of - ) - with transactions_archive_path.open("w") as tf: - cur.copy_expert( - "copy (select * from app_registertransaction) to STDOUT with csv delimiter ';'", - tf - ) - # delete - cur.execute("delete from app_order;") - cur.execute("delete from app_registertransaction;") - connection.commit() - # # # # # - exit_code = 0 - print("done.") - except (Error, Exception) as err: - connection.rollback() - print(f"An error occured while upgrading the database at {os.environ['PGDB_HOST']}:\n{err}") - exit_code = 1 - finally: - cur.close() - connection.close() - exit(exit_code) diff --git a/lib/auto-upgrade-db.sh b/lib/auto-upgrade-db.sh deleted file mode 100644 index beadb59..0000000 --- a/lib/auto-upgrade-db.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - - -echo -e "Checking if database needs an upgrade..." - -if python3 $(pwd)/lib/verify-db-app-version.py; then - - echo -e "No database upgrade needed." - -else - - echo -e "Starting automatic database upgrade..." - source "$(pwd)/lib/db-migrations.sh" - python3 $(pwd)/lib/upgrade-db.py - -fi diff --git a/lib/bootstrap.py b/lib/bootstrap.py deleted file mode 100644 index 23f0d34..0000000 --- a/lib/bootstrap.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 - - -from os import environ -from os import getcwd -from shlex import quote -from signal import SIGINT -from subprocess import run -from subprocess import Popen -from sys import argv -from sys import stdout -from sys import stderr - - -# some vars -devel = False -caddy_process = None -scs_process = None -app_process = None - - -def stop(): - print("\n\nStopping services.\n\n") - caddy_process.send_signal(SIGINT) - scs_process.send_signal(SIGINT) - app_process.send_signal(SIGINT) - print(f"Caddy stopped with exit code {caddy_process.wait()}.") - print(f"session-clear-scheduler stopped with exit code {scs_process.wait()}.") - if devel: - print(f"Django stopped with exit code {app_process.wait()}.") - else: - print(f"Django/Uvicorn stopped with exit code {app_process.wait()}.") - if caddy_process.returncode != 0 or scs_process.returncode != 0 or app_process.returncode !=0: - exit(1) - else: - exit(0) - - -if __name__ == "__main__": - # development or production environment? - try: - if argv[1] == "devel": - devel = True - except IndexError: - pass - # vars - pwd = getcwd() - APPLICATION_LOG = environ["APPLICATION_LOG"] - CADDY_ACCESS_LOG = environ["CADDY_ACCESS_LOG"] - CADDY_LOG = environ["CADDY_LOG"] - DJANGO_PORT = environ["DJANGO_PORT"] - HTTPS_PORT = environ["HTTPS_PORT"] - if devel: - environ["DJANGO_DEBUG"] = "true" - else: - environ["DJANGO_DEBUG"] = "false" - # info - print(f"\n\nStarting server on port {HTTPS_PORT}...\nYou should be able to access the application locally at https://127.0.0.1:{HTTPS_PORT}/\n\nPress Ctrl+C to stop all services.\n\n") - if not devel: - print(f"All further messages will be written to {APPLICATION_LOG} and {CADDY_LOG}") - print(f"HTTP Access Log will be written to {CADDY_ACCESS_LOG}") - try: - # start django/uvicorn - if devel: - run( - ["python3", f"{pwd}/application/manage.py", "collectstatic", "--noinput"], - stdout=stdout, - stderr=stderr, - env=environ - ) - app_process = Popen( - ["python3", f"{pwd}/application/manage.py", "runserver", f"127.0.0.1:{DJANGO_PORT}"], - stdout=stdout, - stderr=stderr, - env=environ - ) - else: - application_log_file = open(APPLICATION_LOG, "a") - run( - ["python3", f"{pwd}/application/manage.py", "collectstatic", "--noinput"], - stdout=application_log_file, - stderr=application_log_file, - env=environ - ) - app_process = Popen( - [ - "python3", "-m", "uvicorn", - "--host", "127.0.0.1", - "--port", quote(DJANGO_PORT), - "drinks_manager.asgi:application" - ], - stdout=application_log_file, - stderr=application_log_file, - cwd=f"{pwd}/application/", - env=environ - ) - # start caddy - if devel: - caddy_log_file = stdout - caddy_log_file_stderr = stderr - else: - caddy_log_file = caddy_log_file_stderr = open(CADDY_LOG, "a") - caddy_process = Popen( - ["caddy", "run", "--config", f"{pwd}/config/Caddyfile"], - stdout=caddy_log_file, - stderr=caddy_log_file_stderr, - env=environ - ) - # start session-clear-scheduler - if devel: - clear_sched_log_file = stdout - clear_sched_log_file_stderr = stderr - else: - clear_sched_log_file = clear_sched_log_file_stderr = open(APPLICATION_LOG, "a") - scs_process = Popen( - ["python3", f"{pwd}/lib/session-clear-scheduler.py"], - stdout=clear_sched_log_file, - stderr=clear_sched_log_file_stderr - ) - caddy_process.wait() - scs_process.wait() - app_process.wait() - except KeyboardInterrupt: - stop() diff --git a/lib/clear-expired-sessions.sh b/lib/clear-expired-sessions.sh deleted file mode 100644 index c10b5ce..0000000 --- a/lib/clear-expired-sessions.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py clearsessions \ No newline at end of file diff --git a/lib/create-admin.sh b/lib/create-admin.sh deleted file mode 100644 index ee46fd4..0000000 --- a/lib/create-admin.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py createsuperuser - -echo -e "done." \ No newline at end of file diff --git a/lib/db-migrations.sh b/lib/db-migrations.sh deleted file mode 100644 index 7c17843..0000000 --- a/lib/db-migrations.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py makemigrations -python3 $(pwd)/application/manage.py makemigrations app -python3 $(pwd)/application/manage.py migrate - -echo -e "done with db migration." \ No newline at end of file diff --git a/lib/env.sh b/lib/env.sh deleted file mode 100644 index 5863525..0000000 --- a/lib/env.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -export DJANGO_SK_ABS_FP="$(pwd)/config/secret_key.txt" -export PROFILE_PICTURES="$(pwd)/profilepictures/" -export STATIC_FILES="$(pwd)/static/" -export APP_VERSION="13" -export PYTHONPATH="$(pwd)/packages/" diff --git a/lib/generate-secret-key.py b/lib/generate-secret-key.py deleted file mode 100644 index 7f98866..0000000 --- a/lib/generate-secret-key.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from pathlib import Path -from secrets import token_bytes -from base64 import b85encode - -# - -override = False -if len(sys.argv) > 1: - if sys.argv[1] == "--override": - override = True - -random_token_length = 128 - -secret_key_fp = Path("config/secret_key.txt") - -# - -if secret_key_fp.exists() and not override: - print(f"Warning: secret_key.txt already exists in directory {secret_key_fp.absolute()}. Won't override.", file=sys.stderr) - exit(1) -else: - print("Generating random secret key...") - random_key = b85encode(token_bytes(random_token_length)) - with secret_key_fp.open("wb") as secret_key_f: - secret_key_f.write(random_key) - print("done.") \ No newline at end of file diff --git a/lib/session-clear-scheduler.py b/lib/session-clear-scheduler.py deleted file mode 100644 index 6096d72..0000000 --- a/lib/session-clear-scheduler.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 - -# This script clears expired sessions in a regular interval -# The interval is defined (in minutes) by config.sh (SESSION_CLEAR_INTERVAL) - -import os - -from pathlib import Path -from subprocess import run -from time import sleep -from datetime import datetime - -try: - - exiting = False - clear_running = False - - print("[session-clear-scheduler] Starting session-clear-scheduler.") - - session_clear_script_fp = Path("lib/clear-expired-sessions.sh") - clear_interval_seconds = int(os.environ["SESSION_CLEAR_INTERVAL"]) * 60 - - sleep(10) # wait some seconds before the first session clean-up - - while True: - - clear_running = True - run(["/bin/sh", session_clear_script_fp.absolute()]) - clear_running = False - - print(f"[session-clear-scheduler: {datetime.now()}] Cleared expired sessions.") - - if exiting: - break - - sleep(clear_interval_seconds) - -except KeyboardInterrupt: - - exiting = True - - if clear_running: - print(f"[session-clear-scheduler: {datetime.now()}] Received SIGINT. Waiting for current clear process to finish.") - sleep(20) # wait some time - - print(f"[session-clear-scheduler: {datetime.now()}] Exiting") - exit(0) diff --git a/lib/setup-application.sh b/lib/setup-application.sh deleted file mode 100644 index 56ae66d..0000000 --- a/lib/setup-application.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -python3 "$(pwd)/lib/generate-secret-key.py" - -source "$(pwd)/lib/db-migrations.sh" - -python3 $(pwd)/lib/upgrade-db.py - -echo -e "\nCreate admin account. Email is optional.\n" -source "$(pwd)/lib/create-admin.sh" - -python3 $(pwd)/application/manage.py collectstatic --noinput diff --git a/lib/start-django-shell.sh b/lib/start-django-shell.sh deleted file mode 100644 index a696310..0000000 --- a/lib/start-django-shell.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# start a django shell - -export DJANGO_DEBUG="true" - -oldcwd="$(pwd)" -echo "Starting a django shell..." -echo -e "--------------------------------------------------------------------------------\n" -"$(pwd)/application/manage.py" shell -echo -e "\n--------------------------------------------------------------------------------" -cd "$oldcwd" \ No newline at end of file diff --git a/lib/upgrade-db.py b/lib/upgrade-db.py deleted file mode 100644 index d07da87..0000000 --- a/lib/upgrade-db.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -import os, sys - -from pathlib import Path - -from psycopg2 import connect -from psycopg2._psycopg import cursor as _cursor -from psycopg2._psycopg import connection as _connection -from psycopg2 import Error -from psycopg2 import IntegrityError -from psycopg2 import errorcodes - - -# setup or upgrade the database - -def log(s, error=False): - if error: - print(f"{s}", file=sys.stderr) - else: - print(f"{s}", file=sys.stdout) - - -def execute_sql_statement(cursor:_cursor, connection:_connection, sql_statement): - try: - cursor.execute(sql_statement) - connection.commit() - except IntegrityError as ie: - if ie.pgcode == errorcodes.UNIQUE_VIOLATION: - log("Skipping one row that already exists.") - connection.rollback() - else: - log(f"An integrity error occured:\n{ie}\nRolling back...", error=True) - connection.rollback() - except Error as e: - log(f"An SQL statement failed while upgrading the database at {os.environ['PGDB_HOST']}:\n{e}", error=True) - connection.rollback() - - -if __name__ == "__main__": - exit_code = 1 - conn = connect( - user = os.environ["PGDB_USER"], - password = os.environ["PGDB_PASSWORD"], - host = os.environ["PGDB_HOST"], - port = os.environ["PGDB_PORT"], - database = os.environ["PGDB_DB"] - ) - cur = conn.cursor() - try: - log("\nSetting up/upgrading database...") - # # # # # - log("Not deleting register_balance. You can delete it via the Admin Panel (Globals -> register_balance), as it is no more used.") - execute_sql_statement(cur, conn, """ - insert into app_global - values ('global_message', 'Here you can set a global message that will be shown to every user.', 0.0, ''); - """) - execute_sql_statement(cur, conn, """ - insert into app_global - values ('admin_info', 'Here you can set am infotext that will be displayed on the admin panel.', 0.0, ''); - """) - execute_sql_statement(cur, conn, """ - create or replace view app_userdeposits_view as - select * from app_registertransaction - where is_user_deposit = true; - """) - # # # # # - # set app_version in file and database - # database - try: - cur.execute(""" - select value from application_info - where key = 'app_version'; - """) - result = cur.fetchone() - if result == None: - cur.execute(f""" - insert into application_info values ('app_version', '{os.environ['APP_VERSION']}'); - """) - conn.commit() - else: - cur.execute(f""" - update application_info set value = '{os.environ['APP_VERSION']}' where key = 'app_version'; - """) - conn.commit() - except Error as err: - if err.pgcode == errorcodes.UNDEFINED_TABLE: - try: - conn.rollback() - cur.execute(""" - create table application_info ( - key varchar(32) primary key, - value text - ); - """) - cur.execute(f""" - insert into application_info values ('app_version', '{os.environ['APP_VERSION']}'); - """) - conn.commit() - except Error as err2: - log(f"An error occurred while setting app_version in table application_info: {err}", error=True) - exit_code = 1 - else: - log(f"An error occurred while setting app_version in table application_info: {err}", error=True) - exit_code = 1 - # file - Path("./config/db_app_version.txt").write_text(os.environ["APP_VERSION"]) - # done - exit_code = 0 - log("done with db setup/upgrade.") - except (Error, Exception) as err: - log(f"An error occured while upgrading the database at {os.environ['PGDB_HOST']}:\n{err}", error=True) - exit_code = 1 - finally: - cur.close() - conn.close() - exit(exit_code) diff --git a/lib/verify-db-app-version.py b/lib/verify-db-app-version.py deleted file mode 100644 index 3fb10f9..0000000 --- a/lib/verify-db-app-version.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 - -from os import environ -from pathlib import Path - -from psycopg2 import connect -from psycopg2._psycopg import cursor -from psycopg2 import Error -from psycopg2 import errorcodes - - -# verify if the installation -# exit code 0 -> no database update is necessary -# exit code 1 -> database update is necessary - - -def check_file(): - db_app_version_file = Path("./config/db_app_version.txt") - if not db_app_version_file.exists(): - exit(1) - if not db_app_version_file.is_file(): - exit(1) - if not db_app_version_file.read_text().strip(" ").strip("\n") == environ["APP_VERSION"]: - exit(1) - - -def check_database(): - connection = connect( - user = environ["PGDB_USER"], - password = environ["PGDB_PASSWORD"], - host = environ["PGDB_HOST"], - port = environ["PGDB_PORT"], - database = environ["PGDB_DB"] - ) - cur = connection.cursor() - try: - # check application version in db - cur.execute(""" - select value from application_info - where key = 'app_version'; - """) - appinfo_result = list(cur.fetchone())[0] - if appinfo_result == None: - cur.close() - connection.close() - exit(1) - if appinfo_result != environ["APP_VERSION"]: - cur.close() - connection.close() - exit(1) - # check rows in app_global - required_rows = [ - "global_message", - "admin_info" - ] - cur.execute(""" - select name from app_global; - """) - table_global_result = list(cur.fetchall()) - cur.close() - connection.close() - existing_rows = [list(row)[0] for row in table_global_result] - for r in required_rows: - if not r in existing_rows: - exit(1) - except Error: - cur.close() - connection.close() - exit(1) - except Exception as e: - print(f"An exception occured: {e}") - cur.close() - connection.close() - exit(1) - - -if __name__ == "__main__": - check_file() - check_database() - exit(0) diff --git a/application/locale/de/LC_MESSAGES/django.mo b/locales/de/LC_MESSAGES/django.mo similarity index 100% rename from application/locale/de/LC_MESSAGES/django.mo rename to locales/de/LC_MESSAGES/django.mo diff --git a/application/locale/de/LC_MESSAGES/django.po b/locales/de/LC_MESSAGES/django.po similarity index 100% rename from application/locale/de/LC_MESSAGES/django.po rename to locales/de/LC_MESSAGES/django.po diff --git a/application/manage.py b/manage.py similarity index 80% rename from application/manage.py rename to manage.py index dce56eb..e170f6b 100755 --- a/application/manage.py +++ b/manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/misc/drinks-manager.service b/misc/drinks-manager.service deleted file mode 100644 index 9dbc463..0000000 --- a/misc/drinks-manager.service +++ /dev/null @@ -1,25 +0,0 @@ -# This is a sample service file for drinks manager - -[Unit] -After=network.target network-online.target -Requires=network-online.target -Description=Drinks Manager - -[Service] -User=drinks-manager -Group=drinks-manager -WorkingDirectory=/srv/drinks-manager/ -# start the server: -ExecStart=/usr/bin/bash -c "/srv/drinks-manager/run.sh server" -# stop the process with a SIGINT: -ExecStop=/usr/bin/bash -c "/bin/kill -2 $MAINPID; /usr/bin/sleep 10" -Restart=on-failure -TimeoutStopSec=40s -LimitNPROC=512 -LimitNOFILE=1048576 -AmbientCapabilities=CAP_NET_BIND_SERVICE -PrivateTmp=true -ProtectSystem=full - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/misc/icons/drinksmanager-icon-1024.png b/misc/icons/drinksmanager-icon-1024.png deleted file mode 100644 index 81233aa..0000000 Binary files a/misc/icons/drinksmanager-icon-1024.png and /dev/null differ diff --git a/misc/icons/drinksmanager-icon.src.svg b/misc/icons/drinksmanager-icon.src.svg deleted file mode 100644 index f231677..0000000 --- a/misc/icons/drinksmanager-icon.src.svg +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - Julian Müller (W13R) - - - - - - diff --git a/packages/.gitkeep b/packages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/profilepictures/default.svg b/profilepictures/default.svg deleted file mode 100644 index 7138ef3..0000000 --- a/profilepictures/default.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - diff --git a/logs/.gitkeep b/project/__init__.py similarity index 100% rename from logs/.gitkeep rename to project/__init__.py diff --git a/application/drinks_manager/asgi.py b/project/asgi.py similarity index 56% rename from application/drinks_manager/asgi.py rename to project/asgi.py index dcfe5ee..486c89b 100644 --- a/application/drinks_manager/asgi.py +++ b/project/asgi.py @@ -1,16 +1,16 @@ """ -ASGI config for drinks_manager project. +ASGI config for project 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/3.2/howto/deployment/asgi/ +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_asgi_application() diff --git a/project/settings.py b/project/settings.py new file mode 100644 index 0000000..cd594ec --- /dev/null +++ b/project/settings.py @@ -0,0 +1,160 @@ +""" +Django settings for project project. + +Generated by 'django-admin startproject' using Django 4.1.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +import os +from pathlib import Path +from yaml import safe_load + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Load configuration file +with Path(BASE_DIR / "data" / "config.yml").open("r") as f: + config = safe_load(f) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config["app"]["secret_key"] +if SECRET_KEY == "!!!replace this with random data!!!" or len(SECRET_KEY) < 40: + print( + "WARNING: You didn't provide a secure secret_key in the configuration file!", + "This is a security risk!!!") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True +if "APP_PROD" in os.environ: + DEBUG = not os.environ["APP_PROD"] + +# ALLOWED_HOSTS can be wildcarded, +# because caddy already handles requests +ALLOWED_HOSTS = ["*"] + +# Application definition + +INSTALLED_APPS = [ + "app.apps.AppConfig", + "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.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "project.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", + "app.context_processors.app_version" + ], + }, + }, +] + +WSGI_APPLICATION = "project.wsgi.application" + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": 'django.db.backends.postgresql', + "NAME": config["db"]["database"], + "USER": config["db"]["user"], + "PASSWORD": config["db"]["password"], + "HOST": config["db"]["host"], + "PORT": str(config["db"]["port"]), + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +if config["app"]["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", + }, + ] +else: + AUTH_PASSWORD_VALIDATORS = [] + +# Security settings + +AUTH_USER_MODEL = "app.User" +SESSION_COOKIE_AGE = int(config["app"]["session_cookie_age"]) +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = config["app"]["language_code"] +TIME_ZONE = config["app"]["timezone"] +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LOCALE_PATHS = [ + BASE_DIR / "locales" +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "data" / "static" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Additional settings + +if "APP_VERSION" in os.environ: + APP_VERSION = os.environ["APP_VERSION"] +else: + APP_VERSION = "unknown" + +CURRENCY_SUFFIX = config["app"]["currency_suffix"] diff --git a/application/drinks_manager/urls.py b/project/urls.py similarity index 86% rename from application/drinks_manager/urls.py rename to project/urls.py index 5bf5958..77d62fb 100644 --- a/application/drinks_manager/urls.py +++ b/project/urls.py @@ -1,7 +1,7 @@ -"""drinks_manager URL Configuration +"""project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.2/topics/http/urls/ + https://docs.djangoproject.com/en/4.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views @@ -13,9 +13,8 @@ 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.urls import path, include urlpatterns = [ path('', include("app.urls")) -] \ No newline at end of file +] diff --git a/application/drinks_manager/wsgi.py b/project/wsgi.py similarity index 56% rename from application/drinks_manager/wsgi.py rename to project/wsgi.py index b42c9aa..b5da491 100644 --- a/application/drinks_manager/wsgi.py +++ b/project/wsgi.py @@ -1,16 +1,16 @@ """ -WSGI config for drinks_manager project. +WSGI config for project 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/3.2/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt index da50c9d..2be275b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django~=3.2.7 -django-currentuser==0.5.3 -psycopg2~=2.9.1 -uvicorn~=0.17.6 +Django~=4.1 +psycopg2~=2.9.5 +uvicorn~=0.20.0 +PyYAML~=6.0 diff --git a/run.sh b/run.sh deleted file mode 100755 index a1664c5..0000000 --- a/run.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - - -function show_dm_help { # $1 = exit code - - echo -e "Usage:\t./run.sh \n" - echo -e "\nCommands:\n" - echo -e " server\t\tstart server" - echo -e " setup\t\t\tset up the application" - echo -e " create-admin\t\tcreate an admin account" - echo -e " generate-secret-key\tgenerate a new random secret key for Django" - echo -e " clear-sessions\tmanually remove all expired sessions from the database" - echo -e " force-db-upgrade\tforce a database migration & upgrade" - echo -e " archive-tables\tarchive (copy & delete) all entries in app_order and app_registertransaction" - echo -e " development-server\tstart Django development server and enable debugging" - echo -e " shell\t\t\tstart a Django shell" - echo -e " help\t\t\tShow this help text\n" - echo -e "\nExamples:\n" - echo -e " ./run.sh server" - echo -e " ./run.sh create-admin" - echo "" - - exit $1 - -} - -# set current working directory -cd $(dirname "$0") - -source "$(pwd)/lib/env.sh" - -echo -e "\n## Drinks Manager" -echo -e "## version $APP_VERSION\n" - - -if [ -z $1 ]; then - - show_dm_help 1 - -else - - source "$(pwd)/config/config.sh" - - if [ $1 = 'server' ]; then - - source "$(pwd)/lib/auto-upgrade-db.sh" - python3 "$(pwd)/lib/bootstrap.py" - - elif [ $1 = 'development-server' ]; then - - source "$(pwd)/lib/auto-upgrade-db.sh" - python3 "$(pwd)/lib/bootstrap.py" devel - - elif [ $1 = 'setup' ]; then - - source "$(pwd)/lib/setup-application.sh" - - elif [ $1 = 'generate-secret-key' ]; then - - python3 "$(pwd)/lib/generate-secret-key.py" --override - - elif [ $1 = 'force-db-upgrade' ]; then - - source "$(pwd)/lib/db-migrations.sh" - python3 "$(pwd)/lib/upgrade-db.py" - - elif [ $1 = 'create-admin' ]; then - - source "$(pwd)/lib/create-admin.sh" - - elif [ $1 = 'clear-sessions' ]; then - - source "$(pwd)/lib/clear-expired-sessions.sh" - echo -e "done." - - elif [ $1 = 'archive-tables' ]; then - - python3 "$(pwd)/lib/archive-tables.py" - - elif [ $1 = 'shell' ]; then - - source "$(pwd)/lib/start-django-shell.sh" - - elif [ $1 = 'help' ]; then - - show_dm_help 0 - - else - - show_dm_help 1 - - fi - -fi \ No newline at end of file diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py new file mode 100755 index 0000000..5a9c81d --- /dev/null +++ b/scripts/bootstrap.py @@ -0,0 +1,151 @@ +#!./venv/bin/python3 +# Copyright 2023 Julian Müller (ChaoticByte) + +import os + +from argparse import ArgumentParser +from atexit import register as register_exithandler +from pathlib import Path +from signal import SIGINT +from subprocess import Popen +from sys import path as sys_path +from time import sleep + +from yaml import safe_load + + +base_directory = Path(__file__).parent.parent +data_directory = base_directory / "data" +logfile_directory = data_directory / "logs" +configuration_file = data_directory / "config.yml" +caddyfile = data_directory / "Caddyfile" +logfile_caddy = logfile_directory / "caddy.log" +logfile_app = logfile_directory / "app.log" + + +class MonitoredSubprocess: + def __init__( + self, + name: str, + commandline: list, + logfile: Path, + environment: dict = os.environ, + max_tries: int = 5, + ): + self.name = name + self.commandline = commandline + self.logfile = logfile + self.environment = environment + self.max_tries = max_tries + self.s = None # the subprocess object + self._tries = 0 + self._stopped = False + + def try_start(self): + if self._tries < self.max_tries: + self._tries += 1 + print(f"Starting {self.name}...") + with self.logfile.open("ab") as l: + self.s = Popen( + self.commandline, + stdout=l, + stderr=l, + env=self.environment) + return True + else: + print(f"Max. tries exceeded ({self.name})!") + # the process must already be stopped at this + # point, so we can set the variable accordingly + self._stopped = True + return False + + def stop(self): + if not self._stopped: + print(f"Stopping {self.name}...") + self.s.terminate() + self._stopped = True + + +def cleanup_procs(processes): + for p in processes: + p.stop() + + +if __name__ == "__main__": + argp = ArgumentParser() + argp.add_argument("--devel", help="Start development server", action="store_true") + args = argp.parse_args() + # Load configuration + with configuration_file.open("r") as f: + config = safe_load(f) + # Prepare + os.chdir(str(base_directory)) + Popen( + ["./venv/bin/python3", "./manage.py", "collectstatic", "--noinput"], env=os.environ).wait() + Popen( + ["./venv/bin/python3", "./manage.py", "migrate", "--noinput"], env=os.environ).wait() + # Start + if args.devel: + p = None + try: + p = Popen(["./venv/bin/python3", "./manage.py", "runserver"], env=os.environ).wait() + except KeyboardInterrupt: + if p is not None: + p.send_signal(SIGINT) + else: + # Caddy configuration via env + environment_caddy = os.environ + environment_caddy["DATADIR"] = str(data_directory.absolute()) + environment_caddy["HTTP_PORT"] = str(config["caddy"]["http_port"]) + environment_caddy["HTTPS_PORT"] = str(config["caddy"]["https_port"]) + environment_caddy["APPLICATION_PORT"] = str(config["app"]["application_port"]) + environment_caddy["ACCESS_LOG"] = config["logs"]["http_access"] + # Application configuration via env + environment_app = os.environ + environment_app["APP_PROD"] = "1" + print("\nRunning in production mode.\n") + # define processes + procs = [ + MonitoredSubprocess( + "Caddy Webserver", + ["caddy", "run", "--config", str(caddyfile)], + logfile_caddy, + environment=environment_caddy + ), + MonitoredSubprocess( + "Drinks-Manager", + [ + "./venv/bin/python3", + "-m", + "uvicorn", + "--host", + "127.0.0.1", + "--port", + str(config["app"]["application_port"]), + "project.asgi:application", + ], + logfile_app, + environment=environment_app + ), + ] + # start processes + for p in procs: + p.try_start() + register_exithandler(cleanup_procs, procs) + # monitor processes + try: + while True: + sleep(1) + for p in procs: + returncode = p.s.poll() + if returncode is None: + continue + else: + print(f"{p.name} stopped with exit code {returncode}.") + if p.try_start() is False: + # stop everything if the process + # has exceeded max. tries + exit() + except KeyboardInterrupt: + print("Received KeyboardInterrupt, exiting...") + exit() diff --git a/scripts/setup-env.sh b/scripts/setup-env.sh new file mode 100755 index 0000000..f700819 --- /dev/null +++ b/scripts/setup-env.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright 2023 Julian Müller (ChaoticByte) + +# change to correct directory, if necessary +script_absolute=$(realpath "$0") +script_directory=$(dirname "$script_absolute") +desired_directory=$(realpath "$script_directory"/..) +if [ "$PWD" != "$desired_directory" ]; then + echo "Changing to project directory..." + cd "$desired_directory" +fi + +echo "Creating venv..." +python3 -m venv ./venv + +echo "Activating venv..." +source ./venv/bin/activate + +echo "Installing dependencies..." +python3 -m pip install -r requirements.txt diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..4fbc464 --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +basedir=$(dirname "$0") +basedir=$(realpath $basedir) +cd "$basedir" + +export PYTHONPATH="$basedir" +export DJANGO_SETTINGS_MODULE="project.settings" +export APP_VERSION="revamp-pre" + +exec ./scripts/bootstrap.py "$@" diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index aeae09f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/favicon.png b/static/favicon.png deleted file mode 100644 index 835e2d1..0000000 Binary files a/static/favicon.png and /dev/null differ