diff --git a/app.py b/app.py index a0d67f0..cb254c7 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ import secrets import time import mimetypes from datetime import datetime, timedelta -from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request +from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort from markupsafe import Markup from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user @@ -14,6 +14,7 @@ from PIL import Image from werkzeug.utils import secure_filename from werkzeug.middleware.proxy_fix import ProxyFix from sqlalchemy import func, extract +from collections import defaultdict, deque app = Flask(__name__) app.config.from_object(Config) @@ -27,6 +28,10 @@ AUTHORIZED_COOKIE_VALUE = app.config.get('AUTHORIZED_COOKIE_VALUE', '80d31cdfe63 os.makedirs(UPLOAD_FOLDER, exist_ok=True) +failed_login_attempts = defaultdict(deque) +MAX_ATTEMPTS = 10 +TIME_WINDOW = 60 * 60 + db = SQLAlchemy(app) socketio = SocketIO(app) login_manager = LoginManager(app) @@ -73,6 +78,26 @@ class Expense(db.Model): added_at = db.Column(db.DateTime, default=datetime.utcnow) receipt_filename = db.Column(db.String(255), nullable=True) +with app.app_context(): + # Twój kod inicjalizacyjny, np. utworzenie konta admina + db.create_all() + from werkzeug.security import generate_password_hash + admin = User.query.filter_by(is_admin=True).first() + username = app.config.get('DEFAULT_ADMIN_USERNAME', 'admin') + password = app.config.get('DEFAULT_ADMIN_PASSWORD', 'admin123') + password_hash = generate_password_hash(password) + if admin: + # Aktualizacja jeśli dane się różnią + if admin.username != username or not check_password_hash(admin.password_hash, password): + admin.username = username + admin.password_hash = password_hash + db.session.commit() + else: + # Brak admina – utwórz nowe konto + admin = User(username=username, password_hash=password_hash, is_admin=True) + db.session.add(admin) + db.session.commit() + @static_bp.route('/static/js/') def serve_js(filename): response = send_from_directory('static/js', filename) @@ -108,6 +133,30 @@ def get_progress(list_id): percent = (purchased_count / total_count * 100) if total_count > 0 else 0 return purchased_count, total_count, percent +# zabezpieczenie logowani do systemy - błędne hasła +def is_ip_blocked(ip): + now = time.time() + attempts = failed_login_attempts[ip] + while attempts and now - attempts[0] > TIME_WINDOW: + attempts.popleft() + return len(attempts) >= MAX_ATTEMPTS + +def register_failed_attempt(ip): + now = time.time() + attempts = failed_login_attempts[ip] + while attempts and now - attempts[0] > TIME_WINDOW: + attempts.popleft() + attempts.append(now) + +def reset_failed_attempts(ip): + failed_login_attempts[ip].clear() + +def attempts_remaining(ip): + attempts = failed_login_attempts[ip] + return max(0, MAX_ATTEMPTS - len(attempts)) + +#################################################### + @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) @@ -158,6 +207,10 @@ def filesizeformat_filter(path): def page_not_found(e): return render_template('404.html'), 404 +@app.errorhandler(403) +def forbidden(e): + return '403 Forbidden', 403 + @app.route('/favicon.svg') def favicon(): svg = ''' @@ -202,25 +255,27 @@ def index_guest(): @app.route('/system-auth', methods=['GET', 'POST']) def system_auth(): - + ip = request.remote_addr next_page = request.args.get('next') or url_for('index_guest') + if is_ip_blocked(ip): + flash('Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.', 'danger') + return render_template('system_auth.html'), 403 + if request.method == 'POST': if request.form['password'] == SYSTEM_PASSWORD: - db.create_all() - if not User.query.filter_by(is_admin=True).first(): - admin_user = User( - username=DEFAULT_ADMIN_USERNAME, - password_hash=generate_password_hash(DEFAULT_ADMIN_PASSWORD), - is_admin=True - ) - db.session.add(admin_user) - db.session.commit() - flash(f'Utworzono konto administratora: login={DEFAULT_ADMIN_USERNAME}, hasło={DEFAULT_ADMIN_PASSWORD}') + reset_failed_attempts(ip) resp = redirect(next_page) resp.set_cookie('authorized', AUTHORIZED_COOKIE_VALUE) return resp - flash('Nieprawidłowe hasło do systemu','danger') + else: + register_failed_attempt(ip) + if is_ip_blocked(ip): + flash('Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.', 'danger') + return render_template('system_auth.html'), 403 + remaining = attempts_remaining(ip) + flash(f'Nieprawidłowe hasło do systemu. Pozostało prób: {remaining}', 'warning') + return render_template('system_auth.html') @app.route('/archive_my_list/') diff --git a/config.py b/config.py index 8523679..7476d15 100644 --- a/config.py +++ b/config.py @@ -8,4 +8,4 @@ class Config: DEFAULT_ADMIN_USERNAME = os.environ.get('DEFAULT_ADMIN_USERNAME', 'admin') DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'uploads') - AUTHORIZED_COOKIE_VALUE = os.environ.get('AUTHORIZED_COOKIE_VALUE', 'cookievalue') \ No newline at end of file + AUTHORIZED_COOKIE_VALUE = os.environ.get('AUTHORIZED_COOKIE_VALUE', 'cookievalue') diff --git a/static/css/style.css b/static/css/style.css index bb722b8..73c08e2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,31 +1,84 @@ - .large-checkbox { - width: 1.5em; - height: 1.5em; - } - .clickable-item { - cursor: pointer; - } - .bg-success { - background-color: #1e7e34 !important; - } - .bg-light { - background-color: #2c2f33 !important; - } +/* --- Rozmiary i kursory --- */ +.large-checkbox { + width: 1.5em; + height: 1.5em; +} +.clickable-item { + cursor: pointer; +} - input[type="file"]::file-selector-button { - background-color: #127429; - color: #fff; - border: none; - padding: 0.5em 1em; - border-radius: 4px; - font-weight: bold; - cursor: pointer; - transition: background 0.2s; - } +/* --- Kolory tła (nadpisane klasy Bootstrapa) --- */ +.bg-success { + background-color: #1e7e34 !important; +} +.bg-light { + background-color: #2c2f33 !important; +} - .bg-success { - background-color: #1e7e34 !important; - } - .bg-light { - background-color: #2c2f33 !important; - } \ No newline at end of file +/* --- Styl przycisku wyboru pliku --- */ +input[type="file"]::file-selector-button { + background-color: #127429; + color: #fff; + border: none; + padding: 0.5em 1em; + border-radius: 4px; + font-weight: bold; + cursor: pointer; + transition: background 0.2s; +} + +/* --- Ciemniejsze alerty Bootstrapa --- */ +.alert-success { + background-color: #225d36 !important; + color: #eaffea !important; + border-color: #174428 !important; +} +.alert-danger { + background-color: #7a1f23 !important; + color: #ffeaea !important; + border-color: #531417 !important; +} +.alert-info { + background-color: #1d3a4d !important; + color: #eaf6ff !important; + border-color: #152837 !important; +} +.alert-warning { + background-color: #665c1e !important; + color: #fffbe5 !important; + border-color: #4d4415 !important; +} + +/* Badge - kolory pasujące do ciemnych alertów */ +.badge.bg-success, .badge.text-bg-success { + background-color: #225d36 !important; /* jak alert-success */ + color: #eaffea !important; +} +.badge.bg-danger, .badge.text-bg-danger { + background-color: #7a1f23 !important; /* jak alert-danger */ + color: #ffeaea !important; +} +.badge.bg-info, .badge.text-bg-info { + background-color: #1d3a4d !important; /* jak alert-info */ + color: #eaf6ff !important; +} +.badge.bg-warning, .badge.text-bg-warning { + background-color: #665c1e !important; /* jak alert-warning */ + color: #fffbe5 !important; +} +.badge.bg-secondary, .badge.text-bg-secondary { + background-color: #343a40 !important; /* ciemny szary */ + color: #e2e3e5 !important; +} +.badge.bg-primary, .badge.text-bg-primary { + background-color: #184076 !important; /* ciemny niebieski */ + color: #e6f0ff !important; +} +.badge.bg-light, .badge.text-bg-light { + background-color: #444950 !important; /* ciemniejszy jasny */ + color: #f8f9fa !important; +} +.badge.bg-dark, .badge.text-bg-dark { + background-color: #181a1b !important; /* bardzo ciemny */ + color: #f8f9fa !important; +} diff --git a/templates/base.html b/templates/base.html index ac7a4ae..96bf32f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,13 +21,18 @@ {% if has_authorized_cookie %} {% if current_user.is_authenticated %} - Zalogowany jako: {{ current_user.username }} + + Zalogowany jako: + {{ current_user.username }} + {% else %} - Przeglądasz jako gość + + Przeglądasz jako + gość + {% endif %} {% endif %} -
{% if current_user.is_authenticated and current_user.is_admin %} ⚙️ Panel admina