From 8152019632be08624a4a28063dc075ce128e3203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 7 Jul 2025 23:17:54 +0200 Subject: [PATCH] =?UTF-8?q?przebudowa=20endpointow=20i=20inne=20porz=C4=85?= =?UTF-8?q?dki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 188 +++++++++++++----- static/css/style.css | 14 ++ static/js/{list_guest.js => list_share.js} | 0 static/js/user_management.js | 14 ++ templates/admin/add_user.html | 24 --- templates/admin/admin_panel.html | 9 +- templates/admin/list_users.html | 33 --- templates/admin/reset_password.html | 21 -- templates/admin/user_management.html | 99 +++++++++ templates/edit_my_list.html | 2 +- templates/{404.html => errors.html} | 8 +- .../{list_guest.html => list_share.html} | 2 +- templates/login.html | 2 +- 13 files changed, 275 insertions(+), 141 deletions(-) rename static/js/{list_guest.js => list_share.js} (100%) create mode 100644 static/js/user_management.js delete mode 100644 templates/admin/add_user.html delete mode 100644 templates/admin/list_users.html delete mode 100644 templates/admin/reset_password.html create mode 100644 templates/admin/user_management.html rename templates/{404.html => errors.html} (50%) rename templates/{list_guest.html => list_share.html} (98%) diff --git a/app.py b/app.py index a65634f..a03038e 100644 --- a/app.py +++ b/app.py @@ -34,6 +34,17 @@ UPLOAD_FOLDER = app.config.get('UPLOAD_FOLDER', 'uploads') ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} AUTHORIZED_COOKIE_VALUE = app.config.get('AUTHORIZED_COOKIE_VALUE', '80d31cdfe63539c9') +PROTECTED_JS_FILES = { + "live.js", + "list_share.js", + "hide_list.js", + "socket_reconnect.js", + "product_suggestion.js", + "expenses.js", + "toggle_button.js", + "user_management.js", +} + os.makedirs(UPLOAD_FOLDER, exist_ok=True) failed_login_attempts = defaultdict(deque) @@ -217,12 +228,10 @@ def require_system_password(): and request.endpoint != 'system_auth' \ and not request.endpoint.startswith('login') \ and request.endpoint != 'favicon': - # specjalny wyjątek dla statycznych, ale sprawdzany ręcznie niżej + if request.endpoint == 'static_bp.serve_js': - # tu sprawdzamy czy to JS, który ma być chroniony - protected_js = ["live.js", "list_guest.js", "hide_list.js", "socket_reconnect.js","product_suggestion.js", "expenses.js", "toggle_button.js"] requested_file = request.view_args.get("filename", "") - if requested_file in protected_js: + if requested_file in PROTECTED_JS_FILES: return redirect(url_for('system_auth', next=request.url)) else: return # pozwól na inne pliki statyczne @@ -238,6 +247,7 @@ def require_system_password(): fixed_url = urlunparse(parsed._replace(netloc=request.host)) return redirect(url_for('system_auth', next=fixed_url)) + @app.template_filter('filemtime') def file_mtime_filter(path): try: @@ -260,11 +270,30 @@ def filesizeformat_filter(path): @app.errorhandler(404) def page_not_found(e): - return render_template('404.html'), 404 + return render_template( + 'errors.html', + code=404, + title="Strona nie znaleziona", + message="Ups! Podana strona nie istnieje lub została przeniesiona." + ), 404 @app.errorhandler(403) def forbidden(e): - return '403 Forbidden', 403 + return render_template( + 'errors.html', + code=403, + title="Brak dostępu", + message="Nie masz uprawnień do wyświetlenia tej strony." + ), 403 + +@app.errorhandler(500) +def internal_error(e): + return render_template( + 'errors.html', + code=500, + title="Błąd serwera", + message="Wystąpił nieoczekiwany błąd. Spróbuj ponownie później." + ), 500 @app.route('/favicon.svg') def favicon(): @@ -276,7 +305,7 @@ def favicon(): return svg, 200, {'Content-Type': 'image/svg+xml'} @app.route('/') -def index_guest(): +def main_page(): now = datetime.utcnow() if current_user.is_authenticated: @@ -318,7 +347,7 @@ def index_guest(): def system_auth(): #ip = request.remote_addr ip = request.access_route[0] - next_page = request.args.get('next') or url_for('index_guest') + next_page = request.args.get('next') or url_for('main_page') if is_ip_blocked(ip): flash('Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.', 'danger') @@ -344,7 +373,7 @@ def toggle_archive_list(list_id): l = ShoppingList.query.get_or_404(list_id) if l.owner_id != current_user.id: flash('Nie masz uprawnień do tej listy', 'danger') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) # Pobieramy parametr archive z query string archive = request.args.get('archive', 'true').lower() == 'true' @@ -357,7 +386,7 @@ def toggle_archive_list(list_id): flash(f'Lista „{l.title}” została przywrócona.', 'success') db.session.commit() - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) @app.route('/edit_my_list/', methods=['GET', 'POST']) @login_required @@ -365,7 +394,7 @@ def edit_my_list(list_id): l = ShoppingList.query.get_or_404(list_id) if l.owner_id != current_user.id: flash('Nie masz uprawnień do tej listy', 'danger') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) if request.method == 'POST': new_title = request.form.get('title') @@ -373,7 +402,7 @@ def edit_my_list(list_id): l.title = new_title.strip() db.session.commit() flash('Zaktualizowano tytuł listy', 'success') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) else: flash('Podaj poprawny tytuł', 'danger') return render_template('edit_my_list.html', list=l) @@ -386,7 +415,7 @@ def toggle_visibility(list_id): if request.is_json or request.method == 'POST': return {'error': 'Unauthorized'}, 403 flash('Nie masz uprawnień do tej listy', 'danger') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) l.is_public = not l.is_public db.session.commit() @@ -401,7 +430,7 @@ def toggle_visibility(list_id): else: flash('Lista została ukryta przed gośćmi', 'info') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) @app.route('/login', methods=['GET', 'POST']) def login(): @@ -410,7 +439,7 @@ def login(): if user and check_password_hash(user.password_hash, request.form['password']): login_user(user) flash('Zalogowano pomyślnie', 'success') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) flash('Nieprawidłowy login lub hasło', 'danger') return render_template('login.html') @@ -419,7 +448,7 @@ def login(): def logout(): logout_user() flash('Wylogowano pomyślnie', 'success') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) @app.route('/create', methods=['POST']) @login_required @@ -467,7 +496,7 @@ def share_list(token): if not shopping_list.is_public: flash('Ta lista nie jest publicznie dostępna', 'danger') - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) items = Item.query.filter_by(list_id=shopping_list.id).all() @@ -479,7 +508,7 @@ def share_list(token): total_expense = sum(e.amount for e in expenses) return render_template( - 'list_guest.html', + 'list_share.html', list=shopping_list, items=items, receipt_files=receipt_files, @@ -497,7 +526,7 @@ def guest_list(list_id): expenses = Expense.query.filter_by(list_id=list_id).all() total_expense = sum(e.amount for e in expenses) return render_template( - 'list_guest.html', + 'list_share.html', list=shopping_list, items=items, receipt_files=receipt_files, @@ -570,7 +599,7 @@ def uploaded_file(filename): @login_required def admin_panel(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) now = datetime.utcnow() user_count = User.query.count() @@ -652,7 +681,7 @@ def admin_panel(): @login_required def delete_list(list_id): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) delete_receipts_for_list(list_id) list_to_delete = ShoppingList.query.get_or_404(list_id) Item.query.filter_by(list_id=list_to_delete.id).delete() @@ -662,63 +691,86 @@ def delete_list(list_id): flash(f'Usunięto listę: {list_to_delete.title}', 'success') return redirect(url_for('admin_panel')) -@app.route('/admin/add_user', methods=['GET', 'POST']) +@app.route('/admin/add_user', methods=['POST']) @login_required def add_user(): if not current_user.is_admin: - return redirect(url_for('index_guest')) - if request.method == 'POST': - username = request.form['username'] - password = generate_password_hash(request.form['password']) - new_user = User(username=username, password_hash=password) - db.session.add(new_user) - db.session.commit() - flash('Dodano nowego użytkownika', 'success') - return redirect(url_for('admin_panel')) - return render_template('admin/add_user.html') + return redirect(url_for('main_page')) + + username = request.form['username'] + password = request.form['password'] + + if not username or not password: + flash('Wypełnij wszystkie pola', 'danger') + return redirect(url_for('list_users')) + + if User.query.filter_by(username=username).first(): + flash('Użytkownik o takiej nazwie już istnieje', 'warning') + return redirect(url_for('list_users')) + + hashed_password = generate_password_hash(password) + new_user = User(username=username, password_hash=hashed_password) + db.session.add(new_user) + db.session.commit() + flash('Dodano nowego użytkownika', 'success') + return redirect(url_for('list_users')) @app.route('/admin/users') @login_required def list_users(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) users = User.query.all() user_count = User.query.count() list_count = ShoppingList.query.count() item_count = Item.query.count() activity_log = ["Utworzono listę: Zakupy weekendowe", "Dodano produkt: Mleko"] - return render_template('admin/list_users.html', users=users, user_count=user_count, list_count=list_count, item_count=item_count, activity_log=activity_log) + return render_template('admin/user_management.html', users=users, user_count=user_count, list_count=list_count, item_count=item_count, activity_log=activity_log) -@app.route('/admin/reset_password/', methods=['GET', 'POST']) +@app.route('/admin/change_password/', methods=['POST']) @login_required def reset_password(user_id): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) + user = User.query.get_or_404(user_id) - if request.method == 'POST': - new_password = generate_password_hash(request.form['password']) - user.password_hash = new_password - db.session.commit() - flash('Hasło zresetowane', 'success') + new_password = request.form['password'] + + if not new_password: + flash('Podaj nowe hasło', 'danger') return redirect(url_for('list_users')) - return render_template('admin/reset_password.html', user=user) + + user.password_hash = generate_password_hash(new_password) + db.session.commit() + flash(f'Hasło dla użytkownika {user.username} zostało zaktualizowane', 'success') + return redirect(url_for('list_users')) @app.route('/admin/delete_user/') @login_required def delete_user(user_id): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) + user = User.query.get_or_404(user_id) + + # Zabezpieczenie: sprawdź ilu adminów + if user.is_admin: + admin_count = User.query.filter_by(is_admin=True).count() + if admin_count <= 1: + flash('Nie można usunąć ostatniego administratora.', 'danger') + return redirect(url_for('list_users')) + db.session.delete(user) db.session.commit() flash('Użytkownik usunięty', 'success') return redirect(url_for('list_users')) + @app.route('/admin/receipts') @login_required def admin_receipts(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) all_files = os.listdir(app.config['UPLOAD_FOLDER']) image_files = [f for f in all_files if allowed_file(f)] return render_template( @@ -731,7 +783,7 @@ def admin_receipts(): @login_required def delete_receipt(filename): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(file_path): os.remove(file_path) @@ -744,7 +796,7 @@ def delete_receipt(filename): @login_required def delete_selected_lists(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) ids = request.form.getlist('list_ids') for list_id in ids: lst = ShoppingList.query.get(int(list_id)) @@ -761,7 +813,7 @@ def delete_selected_lists(): @login_required def archive_list(list_id): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) l = ShoppingList.query.get_or_404(list_id) l.is_archived = True db.session.commit() @@ -772,7 +824,7 @@ def archive_list(list_id): @login_required def delete_all_items(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) Item.query.delete() db.session.commit() flash('Usunięto wszystkie produkty', 'success') @@ -782,7 +834,7 @@ def delete_all_items(): @login_required def edit_list(list_id): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) l = ShoppingList.query.get_or_404(list_id) expenses = Expense.query.filter_by(list_id=list_id).all() @@ -841,7 +893,7 @@ def edit_list(list_id): @login_required def list_products(): if not current_user.is_admin: - return redirect(url_for('index_guest')) + return redirect(url_for('main_page')) items = Item.query.order_by(Item.id.desc()).all() users = User.query.all() @@ -989,6 +1041,42 @@ def admin_expenses_data(): response.headers["Cache-Control"] = "no-store, no-cache" return response +@app.route('/admin/promote_user/') +@login_required +def promote_user(user_id): + if not current_user.is_admin: + return redirect(url_for('main_page')) + user = User.query.get_or_404(user_id) + user.is_admin = True + db.session.commit() + flash(f'Użytkownik {user.username} został ustawiony jako admin.', 'success') + return redirect(url_for('list_users')) + +@app.route('/admin/demote_user/') +@login_required +def demote_user(user_id): + if not current_user.is_admin: + return redirect(url_for('main_page')) + user = User.query.get_or_404(user_id) + + # Nie pozwalamy zdegradować siebie + if user.id == current_user.id: + flash('Nie możesz zdegradować samego siebie!', 'danger') + return redirect(url_for('list_users')) + + # Zabezpieczenie: sprawdź ilu jest adminów + admin_count = User.query.filter_by(is_admin=True).count() + if admin_count <= 1 and user.is_admin: + flash('Nie można zdegradować. Musi pozostać co najmniej jeden administrator.', 'danger') + return redirect(url_for('list_users')) + + user.is_admin = False + db.session.commit() + flash(f'Użytkownik {user.username} został zdegradowany.', 'success') + return redirect(url_for('list_users')) + + + # chyba do usuniecia przeniesione na eventy socket.io @app.route('/update-note/', methods=['POST']) def update_note(item_id): diff --git a/static/css/style.css b/static/css/style.css index 9871f51..bf275f3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -159,3 +159,17 @@ input.form-control { border-radius: 10px 10px 0 0; } } + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive table { + min-width: 1000px; +} + +.bg-dark .form-control::placeholder { + color: #ccc !important; + opacity: 1; +} \ No newline at end of file diff --git a/static/js/list_guest.js b/static/js/list_share.js similarity index 100% rename from static/js/list_guest.js rename to static/js/list_share.js diff --git a/static/js/user_management.js b/static/js/user_management.js new file mode 100644 index 0000000..8327c93 --- /dev/null +++ b/static/js/user_management.js @@ -0,0 +1,14 @@ +document.addEventListener('DOMContentLoaded', function() { + var resetPasswordModal = document.getElementById('resetPasswordModal'); + resetPasswordModal.addEventListener('show.bs.modal', function (event) { + var button = event.relatedTarget; + var userId = button.getAttribute('data-user-id'); + var username = button.getAttribute('data-username'); + + var modalTitle = resetPasswordModal.querySelector('#resetUsernameLabel strong'); + modalTitle.textContent = username; + + var form = resetPasswordModal.querySelector('#resetPasswordForm'); + form.action = '/admin/change_password/' + userId; + }); +}); \ No newline at end of file diff --git a/templates/admin/add_user.html b/templates/admin/add_user.html deleted file mode 100644 index 7322128..0000000 --- a/templates/admin/add_user.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Dodaj użytkownika{% endblock %} -{% block content %} - -
-

➕ Dodaj nowego użytkownika

- ← Powrót do panelu -
- -
-
-
-
- -
-
- -
- -
-
-
- -{% endblock %} diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 7d18ab4..9e073d7 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -9,7 +9,7 @@