Complete project revamp with a bunch of commits #37
29
.gitignore
vendored
|
@ -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
|
||||
|
|
42
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).
|
||||
...
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
6
app/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "app"
|
267
app/migrations/0001_initial.py
Normal file
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
@ -16,8 +16,6 @@ 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),
|
|
@ -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)
|
|
@ -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'
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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"]
|
|
@ -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"
|
|
@ -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 {
|
37
data/config.example.yml
Normal file
|
@ -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"
|
|
@ -1,81 +0,0 @@
|
|||
# Commands
|
||||
|
||||
You run a command with
|
||||
|
||||
```
|
||||
./run.sh <command>
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
|
@ -1,14 +0,0 @@
|
|||
# Configuration
|
||||
|
||||
## Main Configuration
|
||||
|
||||
<u>`config/config.sh`</u>
|
||||
|
||||
There is a sample configuration with explanations: [/config/config.sample.sh](/config/config.sample.sh)
|
||||
|
||||
|
||||
## Caddy (Reverse Proxy & Static File Server)
|
||||
|
||||
<u>[config/Caddyfile](/config/Caddyfile)</u>
|
||||
|
||||
The default configuration should work out of the box, don't edit this file unless you know what you're doing.
|
110
docs/Setup.md
|
@ -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.
|
||||
|
||||
<u>**Warning:**</u>
|
||||
|
||||
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 <drinks_manager_directory>
|
||||
```
|
||||
|
||||
|
||||
## 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 '<a safe 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/`.
|
|
@ -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"
|
|
@ -1,6 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
source ./lib/env.sh
|
||||
source ./config/config.sh
|
||||
export DJANGO_DEBUG=true
|
||||
export PYTHONPATH="./packages"
|
|
@ -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)
|
|
@ -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
|
124
lib/bootstrap.py
|
@ -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()
|
|
@ -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
|
|
@ -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."
|
|
@ -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."
|
|
@ -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/"
|
|
@ -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.")
|
|
@ -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)
|
|
@ -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
|
|
@ -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"
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
Before Width: | Height: | Size: 100 KiB |
|
@ -1,113 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
sodipodi:docname="drinksmanager-icon.src.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:zoom="11.313709"
|
||||
inkscape:cx="18.738329"
|
||||
inkscape:cy="21.434173"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1135"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg5">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid9" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3218">
|
||||
<stop
|
||||
style="stop-color:#ffc64a;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3214" />
|
||||
<stop
|
||||
style="stop-color:#e63a44;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3216" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3218"
|
||||
id="linearGradient47271"
|
||||
x1="6.0854168"
|
||||
y1="6.3499999"
|
||||
x2="6.3499999"
|
||||
y2="9.2604933"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,-1.0584098)" />
|
||||
<filter
|
||||
inkscape:collect="always"
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter3494"
|
||||
x="-0.15966609"
|
||||
y="-0.24991529"
|
||||
width="1.3193322"
|
||||
height="1.4998306">
|
||||
<feGaussianBlur
|
||||
inkscape:collect="always"
|
||||
stdDeviation="0.4683102"
|
||||
id="feGaussianBlur3496" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
id="path13"
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#6f6f6f;stroke-width:0.79375;stroke-linejoin:round;stroke-opacity:1;filter:url(#filter3494)"
|
||||
inkscape:label="glass shadow"
|
||||
d="m 11.1125,5.2915901 c 0,2.6302562 -2.1322439,4.7624999 -4.7625001,4.7624999 -2.6302561,0 -4.7625,-2.1322438 -4.7624998,-4.7624999 L 1.5875,3.96875 h 9.525 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
id="path47880"
|
||||
style="opacity:1;fill:#d3ecec;fill-opacity:1;stroke:#d3ecec;stroke-width:0.79375;stroke-linejoin:round;stroke-opacity:1"
|
||||
inkscape:label="glass"
|
||||
d="m 11.1125,5.2915901 c 0,2.6302562 -2.1322439,4.7624999 -4.7625001,4.7624999 -2.6302561,0 -4.7625,-2.1322438 -4.7624998,-4.7624999 L 1.5875,3.96875 h 9.525 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
id="path36439"
|
||||
style="opacity:1;fill:url(#linearGradient47271);fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linejoin:round"
|
||||
inkscape:label="drink"
|
||||
d="m 11.1125,5.2915901 c 0,2.6302562 -2.1322439,4.7624999 -4.7625001,4.7624999 -2.6302561,0 -4.7625,-2.1322438 -4.7624998,-4.7624999 0,0 2.1134564,-0.2957782 3.1749999,-0.2645832 1.2467196,0.036637 2.4571869,0.5028338 3.7041667,0.5291666 C 9.3528123,5.5748866 11.1125,5.2915901 11.1125,5.2915901 Z"
|
||||
sodipodi:nodetypes="cccssc"
|
||||
sodipodi:insensitive="true" />
|
||||
<metadata
|
||||
id="metadata5638">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Julian Müller (W13R)</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.9 KiB |
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<circle
|
||||
style="fill:#808080;fill-opacity:1;stroke:#fffcfe;stroke-opacity:1"
|
||||
id="path848"
|
||||
cx="8"
|
||||
cy="4.5"
|
||||
r="2.5" />
|
||||
<path
|
||||
style="fill:#7f7f7f;fill-opacity:1;stroke:#fffcff;stroke-opacity:1"
|
||||
id="path3433"
|
||||
d="m -3,-13.499699 a 5,5 0 0 1 -2.5,4.3301274 5,5 0 0 1 -5,0 5,5 0 0 1 -2.5,-4.3301274 h 5 z"
|
||||
transform="scale(-1)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 740 B |
|
@ -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()
|
160
project/settings.py
Normal file
|
@ -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"]
|
|
@ -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,7 +13,6 @@ 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 = [
|
|
@ -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()
|
|
@ -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
|
||||
|
|
94
run.sh
|
@ -1,94 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
function show_dm_help { # $1 = exit code
|
||||
|
||||
echo -e "Usage:\t./run.sh <command>\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
|
151
scripts/bootstrap.py
Executable file
|
@ -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()
|
20
scripts/setup-env.sh
Executable file
|
@ -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
|
11
start.sh
Executable file
|
@ -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 "$@"
|
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 41 KiB |