v2.1: Security - Captcha, Admin-Login, Auth-Decorator
This commit is contained in:
+1
-2
@@ -2,7 +2,7 @@ FROM python:3.12-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip install --no-cache-dir flask flask-cors requests
|
RUN pip install --no-cache-dir flask flask-cors werkzeug requests
|
||||||
|
|
||||||
COPY app/ ./
|
COPY app/ ./
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ EXPOSE 8080
|
|||||||
|
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
ENV DATA_DIR=/data
|
ENV DATA_DIR=/data
|
||||||
ENV OLLAMA_URL=http://192.168.0.150:11434
|
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
with open('/root/reservation-system/app/main.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Add auth import
|
||||||
|
old_import = "from database import get_db, init_db, generate_booking_number, log_change"
|
||||||
|
new_import = """from database import get_db, init_db, generate_booking_number, log_change
|
||||||
|
from auth import require_auth, verify_captcha
|
||||||
|
from login_routes import auth_bp"""
|
||||||
|
content = content.replace(old_import, new_import)
|
||||||
|
|
||||||
|
# Add secret key config after app creation
|
||||||
|
old_cors = "CORS(app)"
|
||||||
|
new_cors = """CORS(app)
|
||||||
|
|
||||||
|
# Security config
|
||||||
|
app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
|
||||||
|
|
||||||
|
# Register auth blueprint
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check_auth():
|
||||||
|
# Public endpoints don't require auth
|
||||||
|
public_endpoints = ['/', '/api/captcha', '/api/rooms', '/api/availability',
|
||||||
|
'/api/health', '/api/admin/login']
|
||||||
|
if request.path in public_endpoints or request.path.startswith('/static/'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Admin endpoints require login
|
||||||
|
if request.path.startswith('/api/admin/') and session.get('user_role') != 'admin':
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401"""
|
||||||
|
content = content.replace(old_cors, new_cors)
|
||||||
|
|
||||||
|
# Protect POST reservations with captcha
|
||||||
|
old_post = ''' if request.method == 'POST':
|
||||||
|
data = request.get_json()'''
|
||||||
|
new_post = ''' if request.method == 'POST':
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Captcha for non-admin bookings
|
||||||
|
if session.get('user_role') != 'admin':
|
||||||
|
if not verify_captcha(data.get('captcha_token'), data.get('captcha_answer')):
|
||||||
|
return jsonify({"error": "Invalid captcha. Please solve the math problem."}), 403'''
|
||||||
|
content = content.replace(old_post, new_post, 1)
|
||||||
|
|
||||||
|
with open('/root/reservation-system/app/main.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Security patches applied!")
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from functools import wraps
|
||||||
|
from flask import session, request, jsonify
|
||||||
|
|
||||||
|
# Config
|
||||||
|
ADMIN_PASSWORD_HASH = os.environ.get('ADMIN_PASSWORD', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi')
|
||||||
|
SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
|
||||||
|
|
||||||
|
def generate_captcha():
|
||||||
|
a = random.randint(1, 15)
|
||||||
|
b = random.randint(1, 15)
|
||||||
|
op = random.choice(['+', '-'])
|
||||||
|
if op == '+':
|
||||||
|
answer = a + b
|
||||||
|
else:
|
||||||
|
answer = a - b
|
||||||
|
token = hashlib.sha256(f"{a}{op}{b}{int(time.time()/600)}captcha".encode()).hexdigest()[:16]
|
||||||
|
return {
|
||||||
|
"question": f"{a} {op} {b} = ?",
|
||||||
|
"token": token,
|
||||||
|
"answer": answer
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify_captcha(token, answer):
|
||||||
|
if not token or not answer:
|
||||||
|
return False
|
||||||
|
stored = session.get('captcha_answer')
|
||||||
|
if stored and str(stored) == str(answer):
|
||||||
|
session.pop('captcha_answer', None)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def require_auth(role='admin'):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if 'user_role' not in session:
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
if role == 'admin' and session['user_role'] != 'admin':
|
||||||
|
return jsonify({"error": "Admin required"}), 403
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def check_admin_password(password):
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
return check_password_hash(ADMIN_PASSWORD_HASH, password)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Login und Captcha Routes"""
|
||||||
|
from flask import Blueprint, request, jsonify, session
|
||||||
|
from auth import generate_captcha, verify_captcha, check_admin_password
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/api/captcha', methods=['GET'])
|
||||||
|
def get_captcha():
|
||||||
|
captcha = generate_captcha()
|
||||||
|
session['captcha_answer'] = captcha['answer']
|
||||||
|
return jsonify({"question": captcha['question'], "token": captcha['token']})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/admin/login', methods=['POST'])
|
||||||
|
def admin_login():
|
||||||
|
data = request.get_json() or {}
|
||||||
|
password = data.get('password', '')
|
||||||
|
|
||||||
|
if check_admin_password(password):
|
||||||
|
session['user_role'] = 'admin'
|
||||||
|
session['login_time'] = __import__('time').time()
|
||||||
|
return jsonify({"status": "ok", "role": "admin"})
|
||||||
|
return jsonify({"error": "Invalid credentials"}), 401
|
||||||
|
|
||||||
|
@auth_bp.route('/api/admin/logout', methods=['POST'])
|
||||||
|
def admin_logout():
|
||||||
|
session.pop('user_role', None)
|
||||||
|
session.pop('login_time', None)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/session', methods=['GET'])
|
||||||
|
def check_session():
|
||||||
|
role = session.get('user_role')
|
||||||
|
if role:
|
||||||
|
return jsonify({"role": role, "logged_in": True})
|
||||||
|
return jsonify({"role": None, "logged_in": False})
|
||||||
+20
@@ -10,6 +10,8 @@ from flask import Flask, request, jsonify, render_template, send_from_directory,
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from database import get_db, init_db, generate_booking_number, log_change
|
from database import get_db, init_db, generate_booking_number, log_change
|
||||||
|
from auth import require_auth, verify_captcha
|
||||||
|
from login_routes import auth_bp
|
||||||
from utils.ollama_client import (
|
from utils.ollama_client import (
|
||||||
parse_email_with_ollama,
|
parse_email_with_ollama,
|
||||||
generate_confirmation_email,
|
generate_confirmation_email,
|
||||||
@@ -21,6 +23,24 @@ app = Flask(__name__,
|
|||||||
static_folder='static')
|
static_folder='static')
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
|
# Security config
|
||||||
|
app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
|
||||||
|
|
||||||
|
# Register auth blueprint
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check_auth():
|
||||||
|
# Public endpoints don't require auth
|
||||||
|
public_endpoints = ['/', '/api/captcha', '/api/rooms', '/api/availability',
|
||||||
|
'/api/health', '/api/admin/login']
|
||||||
|
if request.path in public_endpoints or request.path.startswith('/static/'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Admin endpoints require login
|
||||||
|
if request.path.startswith('/api/admin/') and session.get('user_role') != 'admin':
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
|
||||||
# Konfiguration
|
# Konfiguration
|
||||||
DEFAULT_OPEN_HOUR = 10 # 10:00
|
DEFAULT_OPEN_HOUR = 10 # 10:00
|
||||||
DEFAULT_CLOSE_HOUR = 23 # 23:00
|
DEFAULT_CLOSE_HOUR = 23 # 23:00
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user