rozszerzone uprawnienia
This commit is contained in:
176
static/js/access_users.js
Normal file
176
static/js/access_users.js
Normal file
@@ -0,0 +1,176 @@
|
||||
(function () {
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
|
||||
const toast = (m, t = 'info') => (window.showToast ? window.showToast(m, t) : console.log(`[${t}]`, m));
|
||||
|
||||
function appendToken(box, user) {
|
||||
const tokensBox = $('.tokens', box);
|
||||
if (!tokensBox || !user?.id || !user?.username) return;
|
||||
const empty = $('.no-perms', box);
|
||||
if (empty) empty.remove();
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-sm btn-outline-secondary rounded-pill token';
|
||||
btn.dataset.userId = user.id;
|
||||
btn.dataset.username = user.username;
|
||||
btn.title = 'Kliknij, aby odebrać dostęp';
|
||||
btn.innerHTML = `@${user.username} <span aria-hidden="true">×</span>`;
|
||||
tokensBox.appendChild(btn);
|
||||
}
|
||||
|
||||
function wantsJSON() {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
};
|
||||
}
|
||||
|
||||
async function postAction(postUrl, nextPath, params) {
|
||||
const form = new FormData();
|
||||
for (const [k, v] of Object.entries(params)) form.set(k, v);
|
||||
form.set('next', nextPath); // dla trybu HTML fallback
|
||||
|
||||
try {
|
||||
const res = await fetch(postUrl, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'same-origin',
|
||||
headers: wantsJSON()
|
||||
});
|
||||
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ok: !!data?.ok, data, status: res.status };
|
||||
}
|
||||
return { ok: res.ok, data: null, status: res.status };
|
||||
} catch (e) {
|
||||
console.error('POST failed', e);
|
||||
return { ok: false, data: null, status: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function initEditor(box) {
|
||||
if (!box || !box.classList?.contains('access-editor')) return;
|
||||
if (box.dataset._accessEditorInit === '1') return;
|
||||
box.dataset._accessEditorInit = '1';
|
||||
|
||||
const postUrl = box.dataset.postUrl || location.pathname;
|
||||
const nextPath = box.dataset.next || location.pathname;
|
||||
const suggestUrl = box.dataset.suggestUrl || '';
|
||||
const grantAction = box.dataset.grantAction || 'grant';
|
||||
const revokeField = box.dataset.revokeField || 'revoke_user_id';
|
||||
|
||||
const tokensBox = $('.tokens', box);
|
||||
const input = $('.access-input', box);
|
||||
const addBtn = $('.access-add', box);
|
||||
|
||||
// współdzielony datalist do sugestii
|
||||
let datalist = $('#userHintsGeneric');
|
||||
if (!datalist) {
|
||||
datalist = document.createElement('datalist');
|
||||
datalist.id = 'userHintsGeneric';
|
||||
document.body.appendChild(datalist);
|
||||
}
|
||||
input?.setAttribute('list', datalist.id);
|
||||
|
||||
const unique = (arr) => Array.from(new Set(arr));
|
||||
const parseUserText = (txt) => unique((txt || '').split(/[\s,;]+/g).map(s => s.trim().replace(/^@/, '').toLowerCase()).filter(Boolean));
|
||||
const debounce = (fn, ms = 200) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
|
||||
|
||||
// Sugestie (GET JSON)
|
||||
const renderHints = (users = []) => { datalist.innerHTML = users.slice(0, 20).map(u => `<option value="${u}">@${u}</option>`).join(''); };
|
||||
let acCtrl = null;
|
||||
const fetchHints = debounce(async (q) => {
|
||||
if (!suggestUrl) return;
|
||||
try {
|
||||
acCtrl?.abort();
|
||||
acCtrl = new AbortController();
|
||||
const res = await fetch(`${suggestUrl}?q=${encodeURIComponent(q || '')}`, { credentials: 'same-origin', signal: acCtrl.signal });
|
||||
if (!res.ok) return renderHints([]);
|
||||
const data = await res.json().catch(() => ({ users: [] }));
|
||||
renderHints(data.users || []);
|
||||
} catch { renderHints([]); }
|
||||
}, 200);
|
||||
|
||||
input?.addEventListener('focus', () => fetchHints(input.value));
|
||||
input?.addEventListener('input', () => fetchHints(input.value));
|
||||
|
||||
// Revoke (klik w token)
|
||||
box.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.token');
|
||||
if (!btn || !box.contains(btn)) return;
|
||||
|
||||
const userId = btn.dataset.userId;
|
||||
const username = btn.dataset.username;
|
||||
if (!userId) return toast('Brak identyfikatora użytkownika.', 'danger');
|
||||
|
||||
btn.disabled = true; btn.classList.add('disabled');
|
||||
const res = await postAction(postUrl, nextPath, { [revokeField]: userId });
|
||||
|
||||
if (res.ok) {
|
||||
btn.remove();
|
||||
if (!$$('.token', box).length && tokensBox) {
|
||||
const empty = document.createElement('span');
|
||||
empty.className = 'no-perms text-warning small';
|
||||
empty.textContent = 'Brak dodanych uprawnień.';
|
||||
tokensBox.appendChild(empty);
|
||||
}
|
||||
toast(`Odebrano dostęp: @${username}`, 'success');
|
||||
} else {
|
||||
btn.disabled = false; btn.classList.remove('disabled');
|
||||
toast(`Nie udało się odebrać dostępu @${username}`, 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Grant (wiele loginów, bez przeładowania strony)
|
||||
async function addUsers() {
|
||||
const users = parseUserText(input?.value);
|
||||
if (!users?.length) return toast('Podaj co najmniej jednego użytkownika', 'warning');
|
||||
|
||||
addBtn.disabled = true;
|
||||
const prevText = addBtn.textContent;
|
||||
addBtn.textContent = 'Dodaję…';
|
||||
|
||||
let okCount = 0, failCount = 0, appended = 0;
|
||||
|
||||
for (const u of users) {
|
||||
const res = await postAction(postUrl, nextPath, { action: grantAction, grant_username: u });
|
||||
if (res.ok) {
|
||||
okCount++;
|
||||
// jeśli backend odda JSON z userem – dolep token live
|
||||
if (res.data?.user) {
|
||||
appendToken(box, res.data.user);
|
||||
appended++;
|
||||
}
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
addBtn.disabled = false;
|
||||
addBtn.textContent = prevText;
|
||||
if (input) input.value = '';
|
||||
|
||||
if (okCount) toast(`Dodano dostęp: ${okCount} użytkownika`, 'success');
|
||||
if (failCount) toast(`Błędy przy dodawaniu: ${failCount}`, 'danger');
|
||||
|
||||
// fallback: jeśli nic nie dolepiliśmy (brak JSON), odśwież, by zobaczyć nowe tokeny
|
||||
if (okCount && appended === 0) {
|
||||
// opóźnij minimalnie, by toast mignął
|
||||
setTimeout(() => location.reload(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
addBtn?.addEventListener('click', addUsers);
|
||||
input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addUsers(); } });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$$('.access-editor').forEach(initEditor);
|
||||
});
|
||||
document.addEventListener('shown.bs.modal', (ev) => {
|
||||
$$('.access-editor', ev.target).forEach(initEditor);
|
||||
});
|
||||
})();
|
254
static/js/lists_access.js
Normal file
254
static/js/lists_access.js
Normal file
@@ -0,0 +1,254 @@
|
||||
(function () {
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
|
||||
|
||||
const filterInput = $('#listFilter');
|
||||
const filterCount = $('#filterCount');
|
||||
const selectAll = $('#selectAll');
|
||||
const bulkTokens = $('#bulkTokens');
|
||||
const bulkInput = $('#bulkUsersInput');
|
||||
const bulkBtn = $('#bulkAddBtn');
|
||||
const datalist = $('#userHints');
|
||||
|
||||
const unique = (arr) => Array.from(new Set(arr));
|
||||
const parseUserText = (txt) => unique((txt || '')
|
||||
.split(/[\s,;]+/g)
|
||||
.map(s => s.trim().replace(/^@/, '').toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const selectedListIds = () =>
|
||||
$$('.row-check:checked').map(ch => ch.dataset.listId);
|
||||
|
||||
const visibleRows = () =>
|
||||
$$('#listsTable tbody tr').filter(r => r.style.display !== 'none');
|
||||
|
||||
// ===== Podpowiedzi (datalist) z DOM-u =====
|
||||
(function buildHints() {
|
||||
const names = new Set();
|
||||
$$('.owner-username').forEach(el => names.add(el.dataset.username));
|
||||
$$('.permitted-username').forEach(el => names.add(el.dataset.username));
|
||||
// również tokeny już wyrenderowane
|
||||
$$('.token[data-username]').forEach(el => names.add(el.dataset.username));
|
||||
datalist.innerHTML = Array.from(names)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(u => `<option value="${u}">@${u}</option>`)
|
||||
.join('');
|
||||
})();
|
||||
|
||||
// ===== Live filter =====
|
||||
function applyFilter() {
|
||||
const q = (filterInput?.value || '').trim().toLowerCase();
|
||||
let shown = 0;
|
||||
$$('#listsTable tbody tr').forEach(tr => {
|
||||
const hay = `${tr.dataset.id || ''} ${tr.dataset.title || ''} ${tr.dataset.owner || ''}`;
|
||||
const ok = !q || hay.includes(q);
|
||||
tr.style.display = ok ? '' : 'none';
|
||||
if (ok) shown++;
|
||||
});
|
||||
if (filterCount) filterCount.textContent = shown ? `Widoczne: ${shown}` : 'Brak wyników';
|
||||
}
|
||||
filterInput?.addEventListener('input', applyFilter);
|
||||
applyFilter();
|
||||
|
||||
// ===== Select all =====
|
||||
selectAll?.addEventListener('change', () => {
|
||||
visibleRows().forEach(tr => {
|
||||
const cb = tr.querySelector('.row-check');
|
||||
if (cb) cb.checked = selectAll.checked;
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Copy share URL =====
|
||||
$$('.copy-share').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const url = btn.dataset.url;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
showToast('Skopiowano link udostępnienia', 'success');
|
||||
} catch {
|
||||
const ta = Object.assign(document.createElement('textarea'), { value: url });
|
||||
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
|
||||
showToast('Skopiowano link udostępnienia', 'success');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Tokenized users field (global – belka) =====
|
||||
function addGlobalToken(username) {
|
||||
if (!username) return;
|
||||
const exists = $(`.user-token[data-user="${username}"]`, bulkTokens);
|
||||
if (exists) return;
|
||||
const token = document.createElement('span');
|
||||
token.className = 'badge rounded-pill text-bg-secondary user-token';
|
||||
token.dataset.user = username;
|
||||
token.innerHTML = `@${username} <button type="button" class="btn btn-sm btn-link p-0 ms-1 text-white">✕</button>`;
|
||||
token.querySelector('button').addEventListener('click', () => token.remove());
|
||||
bulkTokens.appendChild(token);
|
||||
}
|
||||
bulkInput?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
parseUserText(bulkInput.value).forEach(addGlobalToken);
|
||||
bulkInput.value = '';
|
||||
}
|
||||
});
|
||||
bulkInput?.addEventListener('change', () => {
|
||||
parseUserText(bulkInput.value).forEach(addGlobalToken);
|
||||
bulkInput.value = '';
|
||||
});
|
||||
|
||||
// ===== Bulk grant (z belki) =====
|
||||
async function bulkGrant() {
|
||||
const lists = selectedListIds();
|
||||
const users = $$('.user-token', bulkTokens).map(t => t.dataset.user);
|
||||
|
||||
if (!lists.length) { showToast('Zaznacz przynajmniej jedną listę', 'warning'); return; }
|
||||
if (!users.length) { showToast('Dodaj przynajmniej jednego użytkownika', 'warning'); return; }
|
||||
|
||||
bulkBtn.disabled = true;
|
||||
bulkBtn.textContent = 'Pracuję…';
|
||||
|
||||
const url = location.pathname + location.search;
|
||||
let ok = 0, fail = 0;
|
||||
|
||||
for (const lid of lists) {
|
||||
for (const u of users) {
|
||||
const form = new FormData();
|
||||
form.set('action', 'grant');
|
||||
form.set('target_list_id', lid);
|
||||
form.set('grant_username', u);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'POST', body: form, credentials: 'same-origin' });
|
||||
if (res.ok) ok++; else fail++;
|
||||
} catch { fail++; }
|
||||
}
|
||||
}
|
||||
|
||||
bulkBtn.disabled = false;
|
||||
bulkBtn.textContent = '➕ Nadaj dostęp';
|
||||
|
||||
showToast(`Gotowe. Sukcesy: ${ok}${fail ? `, błędy: ${fail}` : ''}`, fail ? 'danger' : 'success');
|
||||
location.reload();
|
||||
}
|
||||
bulkBtn?.addEventListener('click', bulkGrant);
|
||||
|
||||
// ===== Per-row "Access editor" (tokeny + dodawanie) =====
|
||||
async function postAction(params) {
|
||||
const url = location.pathname + location.search;
|
||||
const form = new FormData();
|
||||
for (const [k, v] of Object.entries(params)) form.set(k, v);
|
||||
const res = await fetch(url, { method: 'POST', body: form, credentials: 'same-origin' });
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
// Delegacja zdarzeń: kliknięcie tokenu = revoke
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.access-editor .token');
|
||||
if (!btn) return;
|
||||
|
||||
const wrapper = btn.closest('.access-editor');
|
||||
const listId = wrapper?.dataset.listId;
|
||||
const userId = btn.dataset.userId;
|
||||
const username = btn.dataset.username;
|
||||
|
||||
if (!listId || !userId) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.classList.add('disabled');
|
||||
|
||||
const ok = await postAction({
|
||||
action: 'revoke',
|
||||
target_list_id: listId,
|
||||
revoke_user_id: userId
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
btn.remove();
|
||||
const tokens = $$('.token', wrapper);
|
||||
if (!tokens.length) {
|
||||
// pokaż info „brak uprawnień”
|
||||
let empty = $('.no-perms', wrapper);
|
||||
if (!empty) {
|
||||
empty = document.createElement('span');
|
||||
empty.className = 'text-warning small no-perms';
|
||||
empty.textContent = 'Brak dodanych uprawnień.';
|
||||
$('.tokens', wrapper).appendChild(empty);
|
||||
}
|
||||
}
|
||||
showToast(`Odebrano dostęp: @${username}`, 'success');
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('disabled');
|
||||
showToast(`Nie udało się odebrać dostępu @${username}`, 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Dodawanie wielu użytkowników per-row
|
||||
document.addEventListener('click', async (e) => {
|
||||
const addBtn = e.target.closest('.access-editor .access-add');
|
||||
if (!addBtn) return;
|
||||
|
||||
const wrapper = addBtn.closest('.access-editor');
|
||||
const listId = wrapper?.dataset.listId;
|
||||
const input = $('.access-input', wrapper);
|
||||
if (!listId || !input) return;
|
||||
|
||||
const users = parseUserText(input.value);
|
||||
if (!users.length) { showToast('Podaj co najmniej jednego użytkownika', 'warning'); return; }
|
||||
|
||||
addBtn.disabled = true;
|
||||
addBtn.textContent = 'Dodaję…';
|
||||
|
||||
let okCount = 0, failCount = 0;
|
||||
|
||||
for (const u of users) {
|
||||
const ok = await postAction({
|
||||
action: 'grant',
|
||||
target_list_id: listId,
|
||||
grant_username: u
|
||||
});
|
||||
if (ok) {
|
||||
okCount++;
|
||||
// usuń info „brak uprawnień”
|
||||
$('.no-perms', wrapper)?.remove();
|
||||
// dodaj token jeśli nie ma
|
||||
const exists = $(`.token[data-username="${u}"]`, wrapper);
|
||||
if (!exists) {
|
||||
const token = document.createElement('button');
|
||||
token.type = 'button';
|
||||
token.className = 'btn btn-sm btn-outline-secondary rounded-pill token';
|
||||
token.dataset.username = u;
|
||||
token.dataset.userId = ''; // nie znamy ID — token nadal klikany, ale bez revoke po ID
|
||||
token.title = '@' + u;
|
||||
token.innerHTML = `@${u} <span aria-hidden="true">×</span>`;
|
||||
$('.tokens', wrapper).appendChild(token);
|
||||
}
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
addBtn.disabled = false;
|
||||
addBtn.textContent = '➕ Dodaj';
|
||||
input.value = '';
|
||||
|
||||
if (okCount) showToast(`Dodano dostęp: ${okCount} użytk.`, 'success');
|
||||
if (failCount) showToast(`Błędy przy dodawaniu: ${failCount}`, 'danger');
|
||||
|
||||
// Odśwież, by mieć poprawne user_id w tokenach (backend wie lepiej)
|
||||
if (okCount) location.reload();
|
||||
});
|
||||
|
||||
// Enter w polu per-row = zadziałaj jak przycisk
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const inp = e.target.closest('.access-editor .access-input');
|
||||
if (inp && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const btn = inp.closest('.access-editor')?.querySelector('.access-add');
|
||||
btn?.click();
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
Reference in New Issue
Block a user