przebudowa endpointow i inne porządki
This commit is contained in:
188
app.py
188
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/<int:list_id>', 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/<int:user_id>', methods=['GET', 'POST'])
|
||||
@app.route('/admin/change_password/<int:user_id>', 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/<int:user_id>')
|
||||
@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/<int:user_id>')
|
||||
@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/<int:user_id>')
|
||||
@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/<int:item_id>', methods=['POST'])
|
||||
def update_note(item_id):
|
||||
|
@@ -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;
|
||||
}
|
14
static/js/user_management.js
Normal file
14
static/js/user_management.js
Normal file
@@ -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;
|
||||
});
|
||||
});
|
@@ -1,24 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Dodaj użytkownika{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">➕ Dodaj nowego użytkownika</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="username" placeholder="Nazwa użytkownika" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">✅ Dodaj użytkownika</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@@ -9,7 +9,7 @@
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark rounded mb-4">
|
||||
<div class="container-fluid p-0">
|
||||
<a class="navbar-brand" href="#">Admin</a>
|
||||
<a class="navbar-brand" href="#">Funkcje:</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNavbar" aria-controls="adminNavbar" aria-expanded="false" aria-label="Przełącz nawigację">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@@ -17,13 +17,10 @@
|
||||
<div class="collapse navbar-collapse" id="adminNavbar">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/add_user">➕ Dodaj użytkownika</a>
|
||||
<a class="nav-link" href="/admin/users">👥 Zarządzanie użytkownikami</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/users">👥 Lista użytkowników</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/receipts">📸 Wszystkie paragony</a>
|
||||
<a class="nav-link" href="/admin/receipts">📸 Paragony</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/products">🛍️ Produkty</a>
|
||||
|
@@ -1,33 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Lista użytkowników{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">👥 Lista użytkowników</h2>
|
||||
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Login</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td class="fw-bold">{{ user.username }}</td>
|
||||
<td>
|
||||
<a href="/admin/reset_password/{{ user.id }}" class="btn btn-sm btn-outline-warning me-1">🔑 Resetuj</a>
|
||||
<a href="/admin/delete_user/{{ user.id }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
@@ -1,21 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Resetuj hasło{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🔑 Resetuj hasło: {{ user.username }}</h2>
|
||||
<a href="/admin/users" class="btn btn-outline-secondary">← Powrót do listy użytkowników</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Nowe hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">💾 Zapisz nowe hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
99
templates/admin/user_management.html
Normal file
99
templates/admin/user_management.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Zarządzanie użytkownikami{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">👥 Zarządzanie użytkownikami</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<!-- Formularz dodawania nowego użytkownika -->
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">➕ Dodaj nowego użytkownika</h5>
|
||||
<form method="post" action="{{ url_for('add_user') }}">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="username" class="form-control bg-dark text-white border-secondary rounded" placeholder="Nazwa użytkownika" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="password" name="password" class="form-control bg-dark text-white border-secondary rounded" placeholder="Hasło" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-outline-success w-100">Dodaj użytkownika</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Login</th>
|
||||
<th>Rola</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td class="fw-bold">{{ user.username }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge bg-primary">Admin</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Użytkownik</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-warning me-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#resetPasswordModal"
|
||||
data-user-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}">
|
||||
🔑 Ustaw hasło
|
||||
</button>
|
||||
{% if not user.is_admin %}
|
||||
<a href="/admin/promote_user/{{ user.id }}" class="btn btn-sm btn-outline-info">⬆️ Ustaw admina</a>
|
||||
{% else %}
|
||||
<a href="/admin/demote_user/{{ user.id }}" class="btn btn-sm btn-outline-secondary">⬇️ Usuń admina</a>
|
||||
{% endif %}
|
||||
<a href="/admin/delete_user/{{ user.id }}" class="btn btn-sm btn-outline-danger me-1">🗑️ Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Modal resetowania hasła -->
|
||||
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<form method="post" id="resetPasswordForm">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title" id="resetPasswordModalLabel">Ustaw hasło</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="resetUsernameLabel">Dla użytkownika: <strong></strong></p>
|
||||
<input type="password" name="password" placeholder="Nowe hasło" class="form-control" required>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="submit" class="btn btn-success w-100">💾 Zapisz nowe hasło</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='user_management.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% endblock %}
|
@@ -8,7 +8,7 @@
|
||||
<input type="text" name="title" id="title" class="form-control" value="{{ list.title }}" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Zapisz</button>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-secondary">Anuluj</a>
|
||||
<a href="{{ url_for('main_page') }}" class="btn btn-secondary">Anuluj</a>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Strona nie znaleziona{% endblock %}
|
||||
{% block title %}Błąd {{ code }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">404 — Strona nie znaleziona</h2>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót na stronę główną</a>
|
||||
<h2 class="mb-2">{{ code }} — {{ title }}</h2>
|
||||
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót na stronę główną</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<p class="fs-4">Ups! Podana strona nie istnieje lub została przeniesiona.</p>
|
||||
<p class="fs-4">{{ message }}</p>
|
||||
<p>Sprawdź adres lub wróć na stronę główną, aby kontynuować.</p>
|
||||
</div>
|
||||
</div>
|
@@ -110,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='list_guest.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='list_share.js') }}"></script>
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
</script>
|
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🔒 Logowanie</h2>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
|
Reference in New Issue
Block a user