rozszerzone uprawnienia

This commit is contained in:
Mateusz Gruszczyński
2025-10-01 10:51:52 +02:00
parent 873e81d95d
commit 01114b4ca9
8 changed files with 908 additions and 380 deletions

View File

@@ -1,179 +0,0 @@
{% extends 'base.html' %}
{% block title %}Zarządzanie dostępem do list{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">🔐{% if list_id %} Zarządzanie dostępem listy #{{ list_id }}{% else %} Zarządzanie dostępem do list {% endif %}</h2>
<div>
{% if list_id %}
<a href="{{ url_for('admin_lists_access') }}" class="btn btn-outline-light me-2">Powrót do wszystkich list</a>
{% endif %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<form method="post">
<input type="hidden" name="action" value="save_changes">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Utworzono</th>
<th scope="col">Statusy</th>
<th scope="col">Udostępnianie</th>
<th scope="col">Uprawnienia</th>
</tr>
</thead>
<tbody>
{% for l in lists %}
<tr>
<td>
{{ l.id }}
<input type="hidden" name="visible_ids" value="{{ l.id }}">
</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}-{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td style="min-width: 220px;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pub_{{ l.id }}"
name="is_public_{{ l.id }}" {% if l.is_public %}checked{% endif %}>
<label class="form-check-label" for="pub_{{ l.id }}">🌐 Publiczna</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="tmp_{{ l.id }}"
name="is_temporary_{{ l.id }}" {% if l.is_temporary %}checked{% endif %}>
<label class="form-check-label" for="tmp_{{ l.id }}">⏳ Tymczasowa</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="arc_{{ l.id }}"
name="is_archived_{{ l.id }}" {% if l.is_archived %}checked{% endif %}>
<label class="form-check-label" for="arc_{{ l.id }}">📦 Archiwalna</label>
</div>
</td>
<td style="min-width: 220px;">
{% if l.share_token %}
{% set share_url = url_for('shared_list', token=l.share_token, _external=True) %}
<div class="d-flex align-items-center gap-2">
<div class="flex-grow-1 text-truncate mono" title="{{ share_url }}">
{{ share_url }}
</div>
</div>
<div class="text-info small">
{% if l.is_public %}Lista widoczna publicznie{% else %}Lista dostępna przez
link/uprawnienia{%
endif %}
</div>
{% else %}
<div class="text-warning small">Brak tokenu</div>
{% endif %}
</td>
<td style="min-width: 320px;">
<ul class="list-group list-group-flush mb-2">
{% for u in permitted_by_list.get(l.id, []) %}
<li
class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
<div>
<span class="fw-semibold">@{{ u.username }}</span>
</div>
<form method="post" class="m-0"
onsubmit="return confirm('Odebrać dostęp @{{ u.username }}?');">
<input type="hidden" name="action" value="revoke">
<input type="hidden" name="target_list_id" value="{{ l.id }}">
<input type="hidden" name="revoke_user_id" value="{{ u.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger">🚫
Odbierz</button>
</form>
</li>
{% endfor %}
{% if permitted_by_list.get(l.id, [])|length == 0 %}
<li class="list-group-item bg-dark text-white border-secondary">
<div class="text-warning small">Brak dodanych uprawnień.</div>
</li>
{% endif %}
</ul>
<!-- Nadawanie dostępu -->
<form method="post" class="m-0">
<input type="hidden" name="action" value="grant">
<input type="hidden" name="target_list_id" value="{{ l.id }}">
<div class="input-group input-group-sm">
<input type="text" name="grant_username"
class="form-control bg-dark text-white border-secondary"
placeholder="nazwa użytkownika">
<button type="submit" class="btn btn-outline-light"> Dodaj</button>
</div>
</form>
</td>
</tr>
{% endfor %}
{% if lists|length == 0 %}
<tr>
<td colspan="7" class="text-center py-4">Brak list do wyświetlenia</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
</div>
</form>
</div>
</div>
{% if not list_id %}
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
</select>
<input type="hidden" name="page" value="{{ page }}">
</form>
<nav aria-label="Nawigacja stron">
<ul class="pagination pagination-dark mb-0">
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link"
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
<a class="page-link"
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
</li>
</ul>
</nav>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,220 @@
{% extends 'base.html' %}
{% block title %}Zarządzanie dostępem do list{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-3">
<h2 class="mb-2">🔐{% if list_id %} Zarządzanie dostępem listy #{{ list_id }}{% else %} Zarządzanie dostępem do list
{% endif %}</h2>
<div class="d-flex gap-2">
{% if list_id %}
<a href="{{ url_for('admin_lists_access') }}" class="btn btn-outline-light">Powrót do wszystkich list</a>
{% endif %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
<!-- STICKY ACTION BAR -->
<div id="bulkBar" class="position-sticky top-0 z-3 mb-3" style="backdrop-filter: blur(6px);">
<div class="card bg-dark border-secondary shadow-sm">
<div class="card-body py-2 d-flex flex-wrap align-items-center gap-3">
<div class="d-flex align-items-center gap-2">
<input id="selectAll" class="form-check-input" type="checkbox" />
<label for="selectAll" class="form-check-label">Zaznacz wszystko</label>
</div>
<div class="vr text-secondary"></div>
<div class="flex-grow-1 d-flex align-items-center gap-2">
<input id="listFilter" class="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="Szukaj po tytule/ID/właścicielu…" aria-label="Filtruj listy">
<span class="text-secondary small ms-1" id="filterCount"></span>
</div>
<div class="vr text-secondary d-none d-md-block"></div>
<!-- BULK GRANT -->
<div class="flex-grow-1">
<div class="input-group input-group-sm">
<input id="bulkUsersInput" class="form-control bg-dark text-white border-secondary"
placeholder="Podaj użytkowników (po przecinku lub enterach)" list="userHints">
<button id="bulkAddBtn" class="btn btn-outline-light" type="button"> Nadaj dostęp</button>
</div>
<div id="bulkTokens" class="d-flex flex-wrap gap-2 mt-2"></div>
</div>
<div class="vr text-secondary d-none d-md-block"></div>
<!-- ZAPISZ ten sam poziom co reszta -->
<div class="d-flex align-items-center gap-2 flex-shrink-0">
<button form="statusForm" type="submit" class="btn btn-sm btn-outline-light">
💾 Zapisz
</button>
</div>
</div>
</div>
</div>
<!-- HINTS -->
<datalist id="userHints"></datalist>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<form id="statusForm" method="post">
<input type="hidden" name="action" value="save_changes">
<div class="table-responsive">
<table class="table table-dark align-middle" id="listsTable">
<thead class="align-middle">
<tr>
<th scope="col" style="width:36px;"></th>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Utworzono</th>
<th scope="col">Statusy</th>
<th scope="col">Udostępnianie</th>
<th scope="col" style="min-width: 340px;">Uprawnienia</th>
</tr>
</thead>
<tbody>
{% for l in lists %}
<tr data-id="{{ l.id }}" data-title="{{ l.title|lower }}"
data-owner="{{ (l.owner.username if l.owner else '-')|lower }}">
<td>
<input class="row-check form-check-input" type="checkbox" data-list-id="{{ l.id }}">
<input type="hidden" name="visible_ids" value="{{ l.id }}">
</td>
<td class="text-nowrap">#{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white text-decoration-none">{{ l.title
}}</a>
</td>
<td>
{% if l.owner %}
👤 <span class="owner-username" data-username="{{ l.owner.username }}">@{{ l.owner.username }}</span>
({{ l.owner.id }})
{% else %}-{% endif %}
</td>
<td class="text-nowrap">{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td style="min-width: 230px;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pub_{{ l.id }}" name="is_public_{{ l.id }}" {% if
l.is_public %}checked{% endif %}>
<label class="form-check-label" for="pub_{{ l.id }}">🌐 Publiczna</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="tmp_{{ l.id }}" name="is_temporary_{{ l.id }}" {%
if l.is_temporary %}checked{% endif %}>
<label class="form-check-label" for="tmp_{{ l.id }}">⏳ Tymczasowa</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="arc_{{ l.id }}" name="is_archived_{{ l.id }}" {%
if l.is_archived %}checked{% endif %}>
<label class="form-check-label" for="arc_{{ l.id }}">📦 Archiwalna</label>
</div>
</td>
<td style="min-width: 260px;">
{% if l.share_token %}
{% set share_url = url_for('shared_list', token=l.share_token, _external=True) %}
<div class="d-flex align-items-center gap-2">
<div class="flex-grow-1 text-truncate mono small" title="{{ share_url }}">{{ share_url }}</div>
<button class="btn btn-sm btn-outline-secondary copy-share" type="button" data-url="{{ share_url }}"
aria-label="Kopiuj link">📋</button>
</div>
<div class="text-info small mt-1">
{% if l.is_public %}Lista widoczna publicznie{% else %}Dostęp przez link / uprawnienia{% endif %}
</div>
{% else %}
<div class="text-warning small">Brak tokenu</div>
{% endif %}
</td>
<td>
<div class="access-editor" data-list-id="{{ l.id }}">
<!-- Tokeny z uprawnieniami -->
<div class="d-flex flex-wrap gap-2 mb-2 tokens">
{% for u in permitted_by_list.get(l.id, []) %}
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill token"
data-user-id="{{ u.id }}" data-username="{{ u.username }}" title="Kliknij, aby odebrać dostęp">
@{{ u.username }} <span aria-hidden="true">×</span>
</button>
{% endfor %}
{% if permitted_by_list.get(l.id, [])|length == 0 %}
<span class="text-warning small no-perms">Brak dodanych uprawnień.</span>
{% endif %}
</div>
<!-- Dodawanie (wiele na raz) -->
<div class="input-group input-group-sm">
<input type="text"
class="form-control form-control-sm bg-dark text-white border-secondary access-input"
placeholder="Dodaj @użytkownika (wiele: przecinki/enter)" list="userHints"
aria-label="Dodaj użytkowników">
<button type="button" class="btn btn-sm btn-outline-light access-add"> Dodaj</button>
</div>
<div class="text-secondary small mt-1">Kliknij token, aby odebrać dostęp.</div>
</div>
</td>
</tr>
{% endfor %}
{% if lists|length == 0 %}
<tr>
<td colspan="8" class="text-center py-4">Brak list do wyświetlenia</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="mt-3 d-flex justify-content-end">
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany statusów</button>
</div>
</form>
</div>
</div>
{% if not list_id %}
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
</select>
<input type="hidden" name="page" value="{{ page }}">
</form>
<nav aria-label="Nawigacja stron">
<ul class="pagination pagination-dark mb-0">
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
</li>
</ul>
</nav>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='lists_access.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -95,67 +95,33 @@
</div>
<!-- DOSTĘP DO LISTY -->
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<h5 class="mb-3">🔐 Dostęp do listy</h5>
<div class="mb-3">
<label class="form-label">👥 Użytkownicy z dostępem</label>
<!-- Link udostępniania -->
<div class="mb-4">
<label class="form-label">🔗 Link udostępniania (wejście przez link daje dostęp; zalogowani dostają
uprawnienia na stałę po kliknięciu w link)</label>
<div class="access-editor border rounded p-2 bg-dark" data-post-url="{{ request.path }}"
data-suggest-url="{{ url_for('edit_my_list_suggestions', list_id=list.id) }}" data-next="{{ request.path }}"
data-list-id="{{ list.id }}">
{% if list.share_token %}
<div class="input-group mb-3">
<input type="text" class="form-control bg-dark text-white border-secondary" readonly
value="{{ url_for('shared_list', token=list.share_token, _external=True) }}" id="sharedListUrl"
aria-label="Udostępniony link">
<a class="btn btn-outline-light" href="{{ url_for('shared_list', token=list.share_token) }}" target="_blank"
title="Otwórz">Otwórz
</a>
</div>
{% else %}
<div class="text-warning small">Brak tokenu udostępniania.</div>
{% endif %}
<div class="text-info small">Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.</div>
</div>
<form method="post" class="m-0">
<div class="row g-3 align-items-end mb-4">
<div class="col-md-6">
<label for="grant_username" class="form-label"> Nadaj dostęp użytkownikowi (login)</label>
<input type="text" name="grant_username" id="grant_username"
class="form-control bg-dark text-white border-secondary rounded" placeholder="np. marek">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-outline-light w-100"> Dodaj</button>
</div>
<input type="hidden" name="action" value="grant">
<input type="hidden" name="next" value="{{ request.path }}">
</div>
</form>
<!-- Lista uprawnionych -->
<div class="mb-3">
<label class="form-label">👥 Użytkownicy z dostępem</label>
{% if permitted_users and permitted_users|length > 0 %}
<ul class="list-group list-group-flush">
{% for u in permitted_users %}
<li
class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
<div>
<span class="fw-semibold">@{{ u.username }}</span>
</div>
<form method="post" onsubmit="return confirm('Odebrać dostęp użytkownikowi @{{ u.username }}?');">
<input type="hidden" name="revoke_user_id" value="{{ u.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger">🚫 Odbierz uprawnienia</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}<br>
<div class="text-warning small">Brak dodanych uprawnień.</div>
<!-- Tokeny uprawnionych -->
<div class="tokens d-flex flex-wrap gap-2 mb-2">
{% for u in permitted_users %}
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill token" data-user-id="{{ u.id }}"
data-username="{{ u.username }}" title="Kliknij, aby odebrać dostęp">
@{{ u.username }} <span aria-hidden="true">×</span>
</button>
{% endfor %}
{% if not permitted_users or permitted_users|length == 0 %}
<span class="no-perms text-warning small">Brak dodanych uprawnień.</span>
{% endif %}
</div>
<!-- Dodawanie (wiele: przecinki/enter) + prywatne podpowiedzi -->
<div class="input-group input-group-sm">
<input type="text" class="access-input form-control form-control-sm bg-dark text-white border-secondary"
placeholder="Dodaj @użytkownika (wiele: przecinki/enter)" aria-label="Dodaj użytkowników">
<button type="button" class="access-add btn btn-sm btn-outline-light"> Dodaj</button>
</div>
<div class="text-secondary small mt-1">Kliknij token, aby odebrać dostęp.</div>
</div>
</div>
@@ -282,4 +248,5 @@
<script src="{{ url_for('static_bp.serve_js', filename='user_receipt_crop.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_crop_logic.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='select.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='access_users.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -244,31 +244,48 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="grantAccessModalLabel">Nadaj dostęp użytkownikowi</h5>
<h5 class="modal-title" id="grantAccessModalLabel">Nadaj dostęp użytkownikom</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<form method="post" action="{{ url_for('list_settings', list_id=list.id) }}" class="m-0">
<div class="modal-body">
<label for="grant_username" class="form-label">👥 Login użytkownika</label>
<input type="text" name="grant_username" id="grant_username"
class="form-control bg-dark text-white border-secondary rounded" placeholder="np. marek" required>
<input type="hidden" name="action" value="grant_access">
<input type="hidden" name="next" value="{{ url_for('view_list', list_id=list.id) }}">
</div>
<div class="modal-body">
<div class="access-editor border rounded p-2 bg-dark"
data-post-url="{{ url_for('list_settings', list_id=list.id) }}"
data-suggest-url="{{ url_for('edit_my_list_suggestions', list_id=list.id) }}"
data-next="{{ url_for('view_list', list_id=list.id) }}" data-list-id="{{ list.id }}"
data-grant-action="grant_access" data-revoke-field="revoke_user_id">
<div class="modal-footer justify-content-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz</button>
<!-- Tokeny aktualnie uprawnionych -->
<div class="tokens d-flex flex-wrap gap-2 mb-2">
{% for u in permitted_users %}
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill token" data-user-id="{{ u.id }}"
data-username="{{ u.username }}" title="Kliknij, aby odebrać dostęp">
@{{ u.username }} <span aria-hidden="true">×</span>
</button>
{% endfor %}
{% if not permitted_users or permitted_users|length == 0 %}
<span class="no-perms text-warning small">Brak dodanych uprawnień.</span>
{% endif %}
</div>
</div>
</form>
<!-- Dodawanie wielu na raz + podpowiedzi prywatne -->
<div class="input-group input-group-sm">
<input type="text" class="access-input form-control form-control-sm bg-dark text-white border-secondary"
placeholder="Dodaj @użytkownika (wiele: przecinki/enter)" aria-label="Dodaj użytkowników">
<button type="button" class="access-add btn btn-sm btn-outline-light"> Dodaj</button>
</div>
<div class="text-secondary small mt-1">Kliknij token, aby odebrać dostęp.</div>
</div>
</div>
<div class="modal-footer justify-content-end">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">Zamknij</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="massAddModal" tabindex="-1" aria-labelledby="massAddModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
@@ -285,7 +302,7 @@
<ul id="mass-add-list" class="list-group"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light btn-sm" data-bs-dismiss="modal">Zamknij</button>
<button type="button" class="btn btn-outline-light btn-sm w-100" data-bs-dismiss="modal">Zamknij</button>
</div>
</div>
</div>
@@ -302,7 +319,7 @@
<script src="{{ url_for('static_bp.serve_js', filename='mass_add.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_upload.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='sort_mode.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='access_users.js') }}?v={{ APP_VERSION }}"></script>
<script>
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
</script>