From 138dac73f386138d93230826522c4fc83a1f8cfb Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Thu, 26 Mar 2026 23:46:53 +0100 Subject: [PATCH] feat: Implement Docker setup with MongoDB and website service, add requirements and build script --- Website/.dockerignore | 15 ++++ Website/Dockerfile | 49 ++++++++++ Website/docker-compose.yml | 35 ++++++++ Website/requirements.txt | 4 + Website/start_docker_build.sh | 35 ++++++++ Website/user.py | 162 +++++++++++++++++++--------------- 6 files changed, 227 insertions(+), 73 deletions(-) create mode 100644 Website/.dockerignore create mode 100644 Website/Dockerfile create mode 100644 Website/docker-compose.yml create mode 100644 Website/requirements.txt create mode 100755 Website/start_docker_build.sh diff --git a/Website/.dockerignore b/Website/.dockerignore new file mode 100644 index 0000000..36a8ee4 --- /dev/null +++ b/Website/.dockerignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +.venv/ +venv/ +build/ +.git/ +.gitignore +mongodb.log +mongodb.log.* +*.dist-info/ +.nuitka-cache/ +*.build/ diff --git a/Website/Dockerfile b/Website/Dockerfile new file mode 100644 index 0000000..e04fc9c --- /dev/null +++ b/Website/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.12-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +ARG NUITKA_JOBS=1 +ARG NUITKA_LOW_MEMORY=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential patchelf \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir nuitka ordered-set zstandard + +COPY . . + +# Build a standalone binary with all required templates/static/data assets. +# Defaults are tuned for lower RAM pressure to avoid host crashes. +RUN set -eu; \ + EXTRA_FLAGS=""; \ + if [ "$NUITKA_LOW_MEMORY" = "1" ]; then \ + EXTRA_FLAGS="--low-memory --lto=no"; \ + fi; \ + python -m nuitka \ + --standalone \ + --follow-imports \ + --assume-yes-for-downloads \ + --jobs="$NUITKA_JOBS" \ + --remove-output \ + --include-data-dir=templates=templates \ + --include-data-dir=static=static \ + --include-data-dir=data=data \ + --output-dir=build \ + $EXTRA_FLAGS \ + main.py + +FROM python:3.12-slim AS runtime + +WORKDIR /app + +COPY --from=builder /app/build/main.dist ./ + +EXPOSE 4999 + +CMD ["./main.bin"] diff --git a/Website/docker-compose.yml b/Website/docker-compose.yml new file mode 100644 index 0000000..4bd62af --- /dev/null +++ b/Website/docker-compose.yml @@ -0,0 +1,35 @@ +services: + mongodb: + image: mongo:7 + restart: unless-stopped + environment: + MONGO_INITDB_DATABASE: Invario_Website + volumes: + - mongo_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] + interval: 10s + timeout: 5s + retries: 8 + + website: + build: + context: . + dockerfile: Dockerfile + args: + NUITKA_JOBS: ${NUITKA_JOBS:-1} + NUITKA_LOW_MEMORY: ${NUITKA_LOW_MEMORY:-1} + restart: unless-stopped + depends_on: + mongodb: + condition: service_healthy + environment: + MONGO_URI: mongodb://mongodb:27017 + MONGO_DB_NAME: Invario_Website + SESSION_COOKIE_SECURE: "0" + JWT_SECRET_KEY: change-this-in-production + ports: + - "4999:4999" + +volumes: + mongo_data: diff --git a/Website/requirements.txt b/Website/requirements.txt new file mode 100644 index 0000000..c131048 --- /dev/null +++ b/Website/requirements.txt @@ -0,0 +1,4 @@ +Flask>=3.0,<4.0 +Flask-JWT-Extended>=4.6,<5.0 +bleach>=6.1,<7.0 +pymongo>=4.8,<5.0 diff --git a/Website/start_docker_build.sh b/Website/start_docker_build.sh new file mode 100755 index 0000000..3be1c35 --- /dev/null +++ b/Website/start_docker_build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: docker is not installed." + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Error: docker compose plugin is not available." + exit 1 +fi + +NUITKA_JOBS="${NUITKA_JOBS:-1}" +NUITKA_LOW_MEMORY="${NUITKA_LOW_MEMORY:-1}" + +echo "==> Safe Nuitka build settings" +echo " NUITKA_JOBS=${NUITKA_JOBS}" +echo " NUITKA_LOW_MEMORY=${NUITKA_LOW_MEMORY}" + +echo "==> Building website image (Nuitka standalone conversion runs in Dockerfile)" +NUITKA_JOBS="$NUITKA_JOBS" NUITKA_LOW_MEMORY="$NUITKA_LOW_MEMORY" docker compose build website + +echo "==> Starting internal MongoDB + Website containers" +docker compose up -d + +echo "==> Container status" +docker compose ps + +echo "Application is starting on: http://localhost:4999" +echo "Use: docker compose logs -f website" +echo "Tip: If build is still too heavy, run with NUITKA_JOBS=1 NUITKA_LOW_MEMORY=1 ./start_docker_build.sh" diff --git a/Website/user.py b/Website/user.py index ce6c21c..caba7e3 100644 --- a/Website/user.py +++ b/Website/user.py @@ -1,8 +1,19 @@ +import os from pymongo import MongoClient import hashlib from bson.objectid import ObjectId +MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017") +MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website") + + +def _get_users_collection(): + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=1500) + db = client[MONGO_DB_NAME] + return client, db["users"] + + def check_password_strength(password): """ Check if a password meets minimum security requirements. @@ -42,13 +53,14 @@ def check_nm_pwd(username, password): Returns: dict: User document if credentials are valid, None otherwise """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - hashed_password = hashlib.sha512(password.encode()).hexdigest() - user = users.find_one({'Username': username, 'Password': hashed_password}) - client.close() - return user + client = None + try: + client, users = _get_users_collection() + hashed_password = hashlib.sha512(password.encode()).hexdigest() + return users.find_one({'Username': username, 'Password': hashed_password}) + finally: + if client: + client.close() def add_user(username, password, name, last_name): @@ -62,14 +74,17 @@ def add_user(username, password, name, last_name): Returns: bool: True if user was added successfully, False if password was too weak """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] if not check_password_strength(password): return False - users.insert_one({'Username': username, 'Password': hashing(password), 'Admin': False, 'active_ausleihung': None, 'name': name, 'last_name': last_name}) - client.close() - return True + + client = None + try: + client, users = _get_users_collection() + users.insert_one({'Username': username, 'Password': hashing(password), 'Admin': False, 'active_ausleihung': None, 'name': name, 'last_name': last_name}) + return True + finally: + if client: + client.close() def make_admin(username): @@ -82,12 +97,14 @@ def make_admin(username): Returns: bool: True if user was promoted successfully """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - users.update_one({'Username': username}, {'$set': {'Admin': True}}) - client.close() - return True + client = None + try: + client, users = _get_users_collection() + users.update_one({'Username': username}, {'$set': {'Admin': True}}) + return True + finally: + if client: + client.close() def remove_admin(username): """ @@ -99,12 +116,14 @@ def remove_admin(username): Returns: bool: True if user was demoted successfully """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - users.update_one({'Username': username}, {'$set': {'Admin': False}}) - client.close() - return True + client = None + try: + client, users = _get_users_collection() + users.update_one({'Username': username}, {'$set': {'Admin': False}}) + return True + finally: + if client: + client.close() def get_user(username): """ @@ -116,12 +135,13 @@ def get_user(username): Returns: dict: User document or None if not found """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - users_return = users.find_one({'Username': username}) - client.close() - return users_return + client = None + try: + client, users = _get_users_collection() + return users.find_one({'Username': username}) + finally: + if client: + client.close() def check_admin(username): @@ -134,12 +154,14 @@ def check_admin(username): Returns: bool: True if user is an administrator, False otherwise """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - user = users.find_one({'Username': username}) - client.close() - return user and user.get('Admin', False) + client = None + try: + client, users = _get_users_collection() + user = users.find_one({'Username': username}) + return user and user.get('Admin', False) + finally: + if client: + client.close() def delete_user(username): """ @@ -152,20 +174,16 @@ def delete_user(username): Returns: bool: True if user was deleted successfully, False otherwise """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - result = users.delete_one({'username': username}) - client.close() - if result.deleted_count == 0: - # Try with different field name - client = MongoClient('localhost', 27017) - db = client['Inventarsystem'] - users = db['users'] - result = users.delete_one({'Username': username}) - client.close() - - return result.deleted_count > 0 + client = None + try: + client, users = _get_users_collection() + result = users.delete_one({'username': username}) + if result.deleted_count == 0: + result = users.delete_one({'Username': username}) + return result.deleted_count > 0 + finally: + if client: + client.close() def get_name(username): """ @@ -174,12 +192,14 @@ def get_name(username): Returns: str: String of name """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - user = users.find_one({'Username': username}) - name = user.get("name") - return name + client = None + try: + client, users = _get_users_collection() + user = users.find_one({'Username': username}) or {} + return user.get("name") + finally: + if client: + client.close() def get_last_name(username): """ @@ -188,12 +208,14 @@ def get_last_name(username): Returns: str: String of last_name """ - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] - user = users.find_one({'Username': username}) - name = user.get("last_name") - return name + client = None + try: + client, users = _get_users_collection() + user = users.find_one({'Username': username}) or {} + return user.get("last_name") + finally: + if client: + client.close() def get_all_users(): @@ -205,9 +227,7 @@ def get_all_users(): list: List of all user documents """ try: - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] + client, users = _get_users_collection() all_users = list(users.find()) client.close() return all_users @@ -228,10 +248,8 @@ def update_password(username, new_password): try: if not check_password_strength(new_password): return False - - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] + + client, users = _get_users_collection() # Hash the new password hashed_password = hashing(new_password) @@ -261,9 +279,7 @@ def update_user_name(username, name, last_name): bool: True if updated successfully, False otherwise """ try: - client = MongoClient('localhost', 27017) - db = client['Invario_Website'] - users = db['users'] + client, users = _get_users_collection() result = users.update_one( {'Username': username},