Owner decision 2026-10-16: the cans feature (bundles of pastes, #4) was half-integrated and has zero prod usage; removing it eliminates four audit findings by deletion. Removed: - internal/api/cans.go and all can routes (/api/pastes/can, /api/cans/*, /can/{id}) plus handleCanPage/renderCan - is_can from /api/mine and /api/public responses - store layer: CanRow, can store funcs, paste_cans UNION in listings, cans sweep; reserved slugs 'can'/'cans' released - templates/can.html, can CSS, is_can badges in history.js/mine.js - cans tests (cans_test.go, cans_flow_test.go, cans_expiry_test.go, TestErrorCodeCanValidation) - docs/api.md cans section, README/LICENSE/docker-compose mentions Store migration: drops paste_cans only when empty (non-empty table is kept and logged; prod verified clean 2026-10-16) and drops the dormant pastes.can_id column. Covered by new TestMigrateDropsCansSchema. Security hardening (audit findings): pw_<id> (unlock) and tok_<id> (deletion-token) cookies now carry Secure: true in addition to HttpOnly + SameSite=Lax; all deployments are HTTPS-only. cookie_set_test now asserts Secure. go vet ./... and go test ./... pass.
47 lines
2.1 KiB
JavaScript
47 lines
2.1 KiB
JavaScript
// #139: saved pastes table.
|
|
function toast(msg, kind) {
|
|
let t = document.querySelector('.toast');
|
|
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
|
|
t.textContent = msg;
|
|
t.classList.remove('success', 'error');
|
|
if (kind === 'success') t.classList.add('success');
|
|
if (kind === 'error') t.classList.add('error');
|
|
t.classList.add('show');
|
|
clearTimeout(t._h);
|
|
t._h = setTimeout(() => t.classList.remove('show'), 2000);
|
|
}
|
|
|
|
const t = PaletteTable.init({
|
|
endpoint: '/api/mine',
|
|
perPage: 25,
|
|
hasPager: true,
|
|
rowHtml: it =>
|
|
`<tr class="row" data-href="/${t.esc(it.id)}"><td>` +
|
|
(it.title
|
|
? `<a class="paste-name" href="/${t.esc(it.id)}">${t.esc(it.title)}</a>`
|
|
: `<a class="slug paste-name" href="/${t.esc(it.id)}">${t.esc(it.id)}</a>`) +
|
|
`</td>` +
|
|
`<td><span class="badge">${t.esc(it.type || it.language || 'text')}</span></td>` +
|
|
`<td class="dim">${t.fmtSize(it.size)}</td><td class="dim" data-ts="${it.created_at}">${t.ago(it.created_at)}</td>` +
|
|
(it.custom_slug ? `<td class="dim"><a class="url-link" href="/${t.esc(it.custom_slug)}">/${t.esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) +
|
|
`<td class="dim"><a class="id-link" href="/${t.esc(it.id)}">${t.esc(it.id)}</a></td>` +
|
|
`<td><button class="btn btn-icon del" data-id="${t.esc(it.id)}" title="Delete paste" aria-label="Delete paste">×</button></td></tr>`,
|
|
emptyFiltered: 'No pastes from this browser match your search.',
|
|
emptyAll: 'No pastes from this browser yet.',
|
|
});
|
|
|
|
// delete buttons (viewer-scoped, enforced server-side #37)
|
|
document.getElementById('rows').addEventListener('click', async e => {
|
|
const del = e.target.closest('button.del');
|
|
if (!del) return;
|
|
e.stopPropagation();
|
|
del.disabled = true;
|
|
try {
|
|
const res = await fetch('/api/pastes/' + del.dataset.id, { method: 'DELETE' });
|
|
if (res.ok) { toast('Deleted', 'success'); t.load(); }
|
|
else { toast('Delete failed', 'error'); del.disabled = false; }
|
|
} catch (err) { toast('Delete failed', 'error'); del.disabled = false; }
|
|
});
|
|
|
|
t.load();
|