ux i nowa funkcja blokady blednych logowan
This commit is contained in:
81
app.py
81
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/<path:filename>')
|
||||
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/<int:list_id>')
|
||||
|
@ -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')
|
||||
AUTHORIZED_COOKIE_VALUE = os.environ.get('AUTHORIZED_COOKIE_VALUE', 'cookievalue')
|
||||
|
@ -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;
|
||||
}
|
||||
/* --- 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;
|
||||
}
|
||||
|
@ -21,13 +21,18 @@
|
||||
|
||||
{% if has_authorized_cookie %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<span class="mx-auto text-white">Zalogowany jako: <strong>{{ current_user.username }}</strong></span>
|
||||
<span class="mx-auto text-white">
|
||||
Zalogowany jako:
|
||||
<span class="badge bg-success">{{ current_user.username }}</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="mx-auto text-white">Przeglądasz jako <strong>gość</strong></span>
|
||||
<span class="mx-auto text-white">
|
||||
Przeglądasz jako
|
||||
<span class="badge bg-info">gość</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-warning btn-sm">⚙️ Panel admina</a>
|
||||
|
Reference in New Issue
Block a user