Complete project revamp with a bunch of commits #37

Merged
ChaoticByte merged 24 commits from revamp into devel 2023-03-26 10:40:59 +00:00
91 changed files with 739 additions and 1345 deletions
Showing only changes of commit 5572fec9c1 - Show all commits

29
.gitignore vendored
View file

@ -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

View file

@ -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).
...

View file

@ -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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app"

View 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,
),
),
],
),
]

View file

@ -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)

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

@ -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)
]
]

View file

@ -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)

View file

@ -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'

View file

@ -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

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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"]

View file

@ -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"

View file

@ -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
View 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"

View file

@ -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
```

View file

@ -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.

View file

@ -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/`.

View file

@ -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"

View file

@ -1,6 +0,0 @@
#!/usr/bin/env bash
source ./lib/env.sh
source ./config/config.sh
export DJANGO_DEBUG=true
export PYTHONPATH="./packages"

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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."

View file

@ -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."

View file

@ -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/"

View file

@ -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.")

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View file

@ -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

View file

View file

@ -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

View file

@ -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
View 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"]

View file

@ -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"))
]
]

View file

@ -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()

View file

@ -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
View file

@ -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
View 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
View 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
View 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 "$@"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB