diff --git a/AGENTS.md b/AGENTS.md index 39d9b1f..4af0930 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,11 +31,11 @@ Primary files: ## Planned Direction -- The existing admin UI/API in RollCalc is planned for removal. -- Do not expand or repair the admin area unless explicitly requested as a temporary measure. +- The previous admin UI/API has been removed from RollCalc. +- Do not reintroduce admin screens or admin APIs unless explicitly requested as a temporary measure. - Keep login/access logging in RollCalc. - Administration and maintenance of `article-data.json` should move to a separate application. -- Treat `templates/admin.html` and `/api/admin/*` expectations as legacy/transitional, not as target architecture. +- Treat article-data administration as out of scope for RollCalc. ## Code Style @@ -73,7 +73,7 @@ These credentials are hardcoded and are a known risk; do not introduce more secr - Access logging writes to `access_log.json` by reading and rewriting the whole file. - The active UI logic is largely inline in `templates/roll_calculator.html`. - Several `static/*.js` files appear to be older, alternative, or integration modules. Confirm script inclusion before modifying them. -- The admin template and backend admin routes are inconsistent and scheduled for removal rather than expansion. +- The previous admin template and backend admin route have been removed; do not rebuild them by default. - The disclaimer confirmation is client-side only. ## Safety Notes diff --git a/PROJECT_KNOWLEDGE.md b/PROJECT_KNOWLEDGE.md index e812080..d3dc7a2 100644 --- a/PROJECT_KNOWLEDGE.md +++ b/PROJECT_KNOWLEDGE.md @@ -14,13 +14,13 @@ Backend: - `app.py` creates the Flask app. - HTTP Basic Auth is implemented with `Flask-HTTPAuth`. -- Users are currently hardcoded in `BETA_USERS` and `ADMIN_USERS`. +- Users are currently configured in `BETA_USERS` with Werkzeug password hashes. - `/` renders the active calculator template. - `/static/` is intended to serve static files behind Basic Auth. - `/api/health` returns health/version information. -- `/api/user` returns the authenticated user and admin flag. -- `/admin/logs` returns access logs for admin users. +- `/api/user` returns the authenticated user. - Access events are appended to `access_log.json`. +- `build_info.json` is loaded at startup and exposed to all templates as `build_info`. Frontend: @@ -29,6 +29,44 @@ Frontend: - `window.ARTICLE_DATA` is loaded from `/static/article-data.json`. - `window.APP_CONFIG` is currently defined inline for forklift rules. - A disclaimer modal blocks use until the user checks the confirmation checkbox. +- The footer displays build metadata: version, branch, commit, and timestamp. + +## Build Info + +Build/deployment metadata is read from: + +```text +build_info.json +``` + +Expected fields: + +- `version` +- `branch` +- `commit` +- `timestamp` + +If `build_info.json` is missing, malformed, or does not contain a usable value, RollCalc falls back to `unknown` for that value and continues serving the site. + +The data is loaded centrally in `app.py` and made available to every template as `build_info`. + +## Authentication + +RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape: + +```python +"username": { + "password_hash": "..." +} +``` + +Password verification uses `werkzeug.security.check_password_hash`. New hashes or config snippets can be generated with: + +```bash +python scripts/manage_users.py username +``` + +Passwords and hashes must not be logged. ## Roll Geometry @@ -184,25 +222,14 @@ This is client-side only. It is not persisted, logged, or enforced server-side. ## Admin Area State -Current backend route: +RollCalc no longer contains an admin UI or admin API. The previous `/admin/logs` route and `templates/admin.html` have been removed. -```text -/admin/logs -``` - -Current `templates/admin.html` expects: - -```text -/api/admin/stats -/api/admin/logs -``` - -Those `/api/admin/*` endpoints are not currently implemented in `app.py`, and there is no route rendering `templates/admin.html`. +Login/access logging remains part of RollCalc, but log viewing and article-data administration should not be implemented inside this app. Maintenance of `article-data.json` is planned for a separate application. ## Known Technical Risks -- Hardcoded plaintext credentials in `app.py`. -- Basic Auth only; no sessions or role framework beyond user dictionaries. +- Password hashes are currently configured in `app.py`; this should eventually move to a protected external configuration or secrets mechanism. +- Basic Auth only; no sessions or role framework beyond the user dictionary. - `access_log.json` is rewritten on every logged request and is not concurrency-safe. - No log rotation or retention policy is implemented. - Flask's default static route may conflict with the intended authenticated `/static/` behavior; verify effective routing before relying on protected static files. diff --git a/README.md b/README.md index bd3eedd..e010721 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If port `5000` is already occupied, start through Flask's CLI without changing f flask --app app run --host 127.0.0.1 --port 5001 ``` -The app uses HTTP Basic Auth. Current credentials are defined in `BETA_USERS` and `ADMIN_USERS` in `app.py`. +The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.py` with Werkzeug password hashes. ## Project Layout @@ -51,14 +51,14 @@ The app uses HTTP Basic Auth. Current credentials are defined in `BETA_USERS` an ├── app.py ├── requirements.txt ├── README.md +├── build_info.json ├── access_log.json ├── config.json ├── article-data_.json ├── fix_article_data.py ├── service-worker.js ├── templates/ -│ ├── roll_calculator.html -│ └── admin.html +│ └── roll_calculator.html ├── static/ │ ├── article-data.json │ ├── config.json @@ -88,10 +88,24 @@ Implemented routes: | `/` | `GET` | Basic Auth | Renders `templates/roll_calculator.html`. | | `/static/` | `GET` | Basic Auth | Intended protected static-file serving from `static/`. | | `/api/health` | `GET` | Basic Auth | Returns app health and version. | -| `/api/user` | `GET` | Basic Auth | Returns current authenticated user and admin flag. | -| `/admin/logs` | `GET` | Basic Auth plus admin check | Returns `access_log.json` contents. | +| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. | -Authentication is implemented with `Flask-HTTPAuth`. The current code merges `BETA_USERS` and `ADMIN_USERS` and performs direct plaintext string comparison. +Authentication is implemented with `Flask-HTTPAuth`. The current code checks `BETA_USERS` with `werkzeug.security.check_password_hash`; plaintext passwords are not stored in the application. + +Generate a password hash or user entry with: + +```bash +python scripts/manage_users.py username +python scripts/manage_users.py username --json +``` + +For non-interactive local maintenance only: + +```bash +python scripts/manage_users.py username --password 'new-password' +``` + +Do not commit real passwords or print them in logs. Access logging is handled by `log_access()`, which reads `access_log.json`, appends a record, and writes the whole file back. @@ -103,6 +117,7 @@ The template contains: - Page layout and all main CSS. - A disclaimer modal shown after Basic Auth login and before calculator use. +- Footer build metadata from `build_info`. - Global state: - `window.ARTICLE_DATA` - `window.APP_CONFIG` @@ -243,20 +258,9 @@ These provide or describe range calculations for material thickness, area weight ### Admin UI -There are admin-oriented HTML files: +RollCalc no longer contains an admin UI or admin API. Login/access logging remains in the backend, but logs are not exposed through a RollCalc admin screen. -- `templates/admin.html` -- `admin.html` -- `admin_simple.html` - -The backend currently exposes `/admin/logs`, but `templates/admin.html` expects: - -```text -/api/admin/stats -/api/admin/logs -``` - -Those `/api/admin/*` routes are not implemented in `app.py` at the time this README was written. There is also no route currently rendering `templates/admin.html`. +Administration and maintenance of `article-data.json` is planned for a separate application. RollCalc should continue to consume `static/article-data.json` read-only. ## Static Assets and Data Files @@ -274,6 +278,19 @@ JSON audit log written by `app.py`. Important implementation detail: each logged access reads and rewrites the entire JSON file. This is simple but not concurrency-safe and can become inefficient as the file grows. +### `build_info.json` + +Deployment/build metadata displayed in the site footer. + +Expected fields: + +- `version` +- `branch` +- `commit` +- `timestamp` + +`app.py` loads this file centrally at startup and exposes it to all templates as `build_info`. If the file is missing, invalid, or a field is empty, the affected values fall back to `unknown` and the website continues to work. + ### `fix_article_data.py` Utility script that updates relative frontend fetch/register paths to Flask-style `/static/...` paths and checks that key static files exist. @@ -285,15 +302,15 @@ Utility script that updates relative frontend fetch/register paths to Flask-styl - There are duplicated or legacy-looking files with similar names. Before editing JavaScript under `static/`, confirm it is actually referenced by the active template. - The documentation under `docs/` contains deployment and feature notes, but some filenames and route assumptions may not match the current app exactly. - The Flask dev server is used for local development only. Production should use a WSGI server. +- Use `scripts/manage_users.py` to create password hashes when adding or rotating Basic Auth users. ## Known Risks and Maintenance Items -- Credentials are hardcoded in `app.py` and should be moved to environment variables or a secrets manager. -- Passwords are stored in plaintext and compared directly. +- Password hashes are currently configured in `app.py`; user configuration should eventually move to environment variables, a protected config file, or a secrets manager. - `access_log.json` is not safe for concurrent writes. - `access_log.json` grows without rotation or retention limits. - The custom `/static/` route is intended to protect static files, but Flask also creates a default static route unless disabled. Verify effective route behavior before relying on static-file protection. -- Admin frontend and backend routes are currently inconsistent. +- RollCalc no longer includes an admin UI/API; article data administration belongs in a separate application. - There is no visible automated test suite. - The main template is large and mixes layout, styling, data loading, calculations, and UI behavior. - The disclaimer confirmation is client-side only and is not persisted or audited server-side. diff --git a/access_log.json b/access_log.json index 9652746..9b2c57f 100644 --- a/access_log.json +++ b/access_log.json @@ -600,5 +600,68 @@ "endpoint": "/", "method": "GET", "status": 200 + }, + { + "timestamp": "2026-07-07T11:44:53.524315", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T11:47:36.702455", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T12:18:47.866971", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T14:46:22.335316", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T16:13:42.026433", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T16:19:41.488752", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T16:34:58.280178", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T16:36:56.896657", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 + }, + { + "timestamp": "2026-07-07T16:41:46.568545", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 } ] \ No newline at end of file diff --git a/app.py b/app.py index 7975f2f..1c250a6 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ Flask app with HTTP Basic Authentication from flask import Flask, render_template, request, send_file, send_from_directory, jsonify from flask_httpauth import HTTPBasicAuth +from werkzeug.security import check_password_hash from functools import wraps import os from datetime import datetime @@ -19,21 +20,65 @@ auth = HTTPBasicAuth() # Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!) BETA_USERS = { - "beta": "rollcalc_beta_2026", # ⚠️ ÄNDERN! - "naue": "naue_access_2026", # ⚠️ ÄNDERN! - "cniehues": "beta_test_2026", # ⚠️ ÄNDERN! - "lvollmert": "Geheim!", # ⚠️ ÄNDERN! - "mtazl": "rollcalc", # ⚠️ ÄNDERN! - "controlling": "beta2026" # ⚠️ ÄNDERN! -} - -# Optional: Admin-Features (z.B. Logs, Stats) -ADMIN_USERS = { - "admin": "admin_secure_pwd_2026" # ⚠️ ÄNDERN! + "beta": { + "password_hash": "pbkdf2:sha256:600000$HNtF3VdZKdmtg8Vw$8f976a99a457c5e924916dc6f735dd631042c8c126af8d4bda9abcd7532d904f" + }, + "naue": { + "password_hash": "pbkdf2:sha256:600000$RVPiW2mYXJJIp3d7$0f838b8619d386e2da57e60ae8ccb944bb0f9f63d81ce3d174ec3034b0abcd19" + }, + "cniehues": { + "password_hash": "pbkdf2:sha256:600000$iNmhakf34xk26xrz$ae78846461370c52b7f56fad5ac0ae8e33860d6f00075ac78e3a5b8c31d51a3d" + }, + "lvollmert": { + "password_hash": "pbkdf2:sha256:600000$8FGop5ymmHpuIqfK$8ba223e20c514cf6bc1297435a2e7e2be81668951297f0e01ad6a931718ad7de" + }, + "mtazl": { + "password_hash": "pbkdf2:sha256:600000$H19skoalhWxlLnY5$d30bfeae38470b19900648fd110978b3677607c3a161d663a7e5d5c2167a3710" + }, + "controlling": { + "password_hash": "pbkdf2:sha256:600000$ksj86lrKz6nnSpXz$5161e0b5c76bd72d6b2cd04866ef49e69f463f474d7e47075171894bf3366f29" + } } # Logging für Auditing LOG_FILE = "access_log.json" +BUILD_INFO_FILE = "build_info.json" +UNKNOWN_BUILD_INFO = { + "version": "unknown", + "branch": "unknown", + "commit": "unknown", + "timestamp": "unknown" +} + +# ============================================================================ +# BUILD INFO +# ============================================================================ + +def load_build_info(): + """Load deployment/build metadata for templates.""" + try: + with open(BUILD_INFO_FILE, "r") as f: + data = json.load(f) + except Exception as e: + print(f"[Build Info Error] {e}") + return UNKNOWN_BUILD_INFO.copy() + + if not isinstance(data, dict): + return UNKNOWN_BUILD_INFO.copy() + + build_info = UNKNOWN_BUILD_INFO.copy() + for key in build_info: + value = data.get(key) + if isinstance(value, str) and value.strip(): + build_info[key] = value.strip() + return build_info + +BUILD_INFO = load_build_info() + +@app.context_processor +def inject_build_info(): + """Make build metadata available in all templates.""" + return {"build_info": BUILD_INFO} # ============================================================================ # AUTHENTICATION @@ -42,8 +87,12 @@ LOG_FILE = "access_log.json" @auth.verify_password def verify_password(username, password): """Verify HTTP Basic Auth credentials""" - all_users = {**BETA_USERS, **ADMIN_USERS} - if username in all_users and all_users[username] == password: + user_config = BETA_USERS.get(username) + if not user_config: + return None + + password_hash = user_config.get("password_hash") + if password_hash and check_password_hash(password_hash, password): return username return None @@ -96,30 +145,12 @@ def health_check(): def get_user(): """Get current authenticated user info""" user = auth.current_user() - is_admin = user in ADMIN_USERS return jsonify({ "username": user, "authenticated": True, - "is_admin": is_admin, "timestamp": datetime.now().isoformat() }), 200 -@app.route("/admin/logs", methods=["GET"]) -@auth.login_required -def get_logs(): - """View access logs - admin only""" - user = auth.current_user() - if user not in ADMIN_USERS: - log_access(user, "/admin/logs", "GET", 403) - return jsonify({"error": "Unauthorized"}), 403 - - log_access(user, "/admin/logs", "GET") - logs = [] - if os.path.exists(LOG_FILE): - with open(LOG_FILE, "r") as f: - logs = json.load(f) - return jsonify({"logs": logs}), 200 - # ============================================================================ # ERROR HANDLERS # ============================================================================ diff --git a/build_info.json b/build_info.json new file mode 100644 index 0000000..c6a437c --- /dev/null +++ b/build_info.json @@ -0,0 +1,6 @@ +{ + "version": "v0.4-dev", + "branch": "develop/v0.4", + "commit": "7a27fb8", + "timestamp": "2026-07-07T16:46:32+02:00" +} diff --git a/scripts/manage_users.py b/scripts/manage_users.py new file mode 100644 index 0000000..2321f27 --- /dev/null +++ b/scripts/manage_users.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +Create password hashes or user config snippets for RollCalc Basic Auth users. +""" + +import argparse +import getpass +import json + +from werkzeug.security import generate_password_hash + + +def build_parser(): + parser = argparse.ArgumentParser( + description="Generate RollCalc password hashes or user config entries." + ) + parser.add_argument( + "username", + nargs="?", + help="Optional username for a generated user config snippet.", + ) + parser.add_argument( + "--password", + help="Password to hash. Omit to enter it securely via prompt.", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output a JSON object for the user entry.", + ) + return parser + + +def main(): + args = build_parser().parse_args() + password = args.password + + if password is None: + password = getpass.getpass("Password: ") + + password_hash = generate_password_hash(password) + + if args.username: + entry = {args.username: {"password_hash": password_hash}} + if args.json: + print(json.dumps(entry, indent=2)) + else: + print(f'"{args.username}": {{') + print(f' "password_hash": "{password_hash}"') + print("}") + else: + print(password_hash) + + +if __name__ == "__main__": + main() diff --git a/scripts/update_build_info.sh b/scripts/update_build_info.sh new file mode 100755 index 0000000..4cb9780 --- /dev/null +++ b/scripts/update_build_info.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$PROJECT_ROOT" + +cat > build_info.json < - - - - - - Admin Panel – Roll Calculator - - - - - -
-
-

🔐 Admin Panel

-
Roll Calculator – Naue GmbH & Co. KG
-
- -
- - -
- - -
-
- ⏳ Statistiken werden geladen... -
-
- - -
- - -
-

📋 Access Logs

-
- - -
-
- - -
- - -
- - -
- - - - - - - - - - - - - - - - -
ZeitstempelBenutzerEndpointMethodeStatus
Logs werden geladen...
- -
- - - - -
- - - - - - diff --git a/templates/roll_calculator.html b/templates/roll_calculator.html index 9bd6ae2..b38ddc1 100644 --- a/templates/roll_calculator.html +++ b/templates/roll_calculator.html @@ -48,6 +48,9 @@ .result-label { font-size: 12px; color: #1F5438; font-weight: 600; text-transform: uppercase; } .result-value { font-size: 28px; font-weight: 700; color: #1F5438; line-height: 1.2; } .result-range { font-size: 12px; color: #1F5438; margin-top: 4px; font-weight: 500; } + .result-range div { margin-top: 2px; } + .result-range .range-warning { color: #9a6700; font-weight: 700; } + .result-range .range-ok { color: #1F5438; font-weight: 700; } .forklift-check { border-radius: 7px; padding: 14px 16px; margin-top: 14px; font-size: 13px; line-height: 1.6; display: none; } .forklift-check.show { display: block; } .forklift-check.fc-ok { background: #eafaf1; border: 1.5px solid #28a745; } @@ -60,6 +63,7 @@ .fc-req-nok { color: #dc3545; font-weight: 600; } .formula-box { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #666; margin-top: 18px; font-family: 'Courier New', monospace; } .footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; margin-top: 30px; } + .footer-build-info { margin-top: 6px; color: #bbb; } .disclaimer-overlay { position: fixed; inset: 0; @@ -571,6 +575,27 @@ kg +
+ +
+ + mm +
+
+
+ +
+ + kg +
+
+
+ +
+ + m +
+
@@ -844,6 +869,9 @@