diff --git a/bin/assets/scripts/delete.js b/bin/assets/scripts/delete.js new file mode 100644 index 0000000..22b492b --- /dev/null +++ b/bin/assets/scripts/delete.js @@ -0,0 +1,37 @@ +const deleteButton = document.getElementById('delete-button'); +const id = location.pathname.slice(1, location.pathname.lastIndexOf('.')); + +const token = await localforage.getItem(id); + +if (token) { + deleteButton.classList.remove('hidden'); + deleteButton.addEventListener('click', async () => { + try { + const response = await fetch(window.location.pathname, { + method: 'DELETE', + headers: { + Authorization: `Token ${token}`, + }, + }); + + if (!response.ok) { + const error = new Error(`${response.status} ${response.statusText}\n${await response.text()}`); + error.response = response; + throw error; + } + + location.href = '/'; + } catch (error) { + console.error(error); + if (error.response.status === 401) { + alert('Le token stocké est très probablement erroné.'); + localforage.removeItem(id); + } else { + console.error("Nous vous recommandons d'ouvrir une issue sur https://github.com/readthedocs-fr/bin-server si le problème persiste."); + alert(`Une erreur est survenue (${error.response?.statusText || error.message}), votre snippet n'a donc pas pu être supprimé. Regardez la console pour plus d'informations.`); + } + } + }); +} else { + deleteButton.remove(); +} diff --git a/bin/assets/scripts/newform.js b/bin/assets/scripts/newform.js index 0a7cad6..5d012c7 100644 --- a/bin/assets/scripts/newform.js +++ b/bin/assets/scripts/newform.js @@ -2,15 +2,12 @@ const form = document.forms['post-snippet']; const lang = form.lang; const langs = [...lang.options].slice(1).flatMap((option) => [option.value, option.textContent.toLowerCase()]); const code = form.code; -const token = form.token; -token.addEventListener('click', () => { - navigator.clipboard.writeText(token.value).catch(() => { - // fallback to execCommand copy method - token.value.select(); - document.execCommand('copy'); - }); -}) +// NOTE: increment this number on Service Worker update +const SW_VERSION = 0; +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js?v=' + SW_VERSION); +} const langAliases = { txt: langs.indexOf('txt'), diff --git a/bin/assets/scripts/sw.js b/bin/assets/scripts/sw.js new file mode 100644 index 0000000..fd7a0dd --- /dev/null +++ b/bin/assets/scripts/sw.js @@ -0,0 +1,41 @@ +importScripts('https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js'); + +const NEW_URL = location.origin + '/new'; + +self.addEventListener('install', () => { + console.log('Installed Service Worker.'); + self.skipWaiting(); +}); +self.addEventListener('activate', () => self.clients.claim()); + +// As the redirection mode of the form POST request +// is 'manual', we cannot get the the redirection URL +// from the request (opaqueredirect). A trick, is to store +// the event.request.resultingClientId, which seems to be +// the same only for the form POST request and the fetch +// of the redirection URL. This allows us to intercept +// the correct redirection URL request and thus obtain +// the ID. +const tokens = new Map(); +self.addEventListener('fetch', (event) => { + let token; + // Intercept the request when the form is submitted + if (event.request.url === NEW_URL && event.request.method === 'POST') { + event.respondWith(fetch(event.request.clone()).then(async (response) => { + // Store the resultingClientId only if the request is successful. + if (response.type === 'opaqueredirect') { + tokens.set(event.request.resultingClientId, + new URLSearchParams(await event.request.text()).get('token')); + } + return response; + })); + + // If the request's resultingClientId is the same as + // the submitted form request's resultingClientId, + // then this request is the fetch of the redirection URL. + } else if (token = tokens.get(event.request.resultingClientId)) { + tokens.delete(event.request.resultingClientId); + localforage.setItem(event.request.url.slice(event.request.url.lastIndexOf('/') + 1, + event.request.url.lastIndexOf('.')), token); + } +}); diff --git a/bin/assets/styles/style.css b/bin/assets/styles/style.css index 5af91a7..51de399 100644 --- a/bin/assets/styles/style.css +++ b/bin/assets/styles/style.css @@ -148,3 +148,7 @@ input[type="number"] { #post-snippet:invalid .controls button svg { fill: #707070; } + +button.hidden { + display: none; +} diff --git a/bin/controller.py b/bin/controller.py index 8627eca..9b0f728 100644 --- a/bin/controller.py +++ b/bin/controller.py @@ -47,6 +47,7 @@ def get_new_form(): return bt.template( 'newform.html', + token=secrets.token_urlsafe(16), languages=languages, default_language=lang, code=code, @@ -65,6 +66,12 @@ def assets(filepath): """ return bt.static_file(filepath, root=root.joinpath('assets')) +@bt.route('/sw.js') +def sw(): + """ + Get the Service Worker with a allowed scope to '/'. + """ + return bt.static_file('/sw.js', root=root.joinpath('assets/scripts')) @bt.route('/new', method='POST') def post_new(): @@ -159,7 +166,6 @@ def get_html(snippetid, ext=None): :param snippetid: (path) required snippet id :param ext: (path) optional language file extension, used to determine the highlight backend - :param token: (query) optional the "admin" token :raises HTTPError: code 404 when the snippet is not found """ @@ -183,7 +189,6 @@ def get_html(snippetid, ext=None): ext=ext, snippetid=snippetid, parentid=snippet.parentid, - token=bt.request.query.token, ) @bt.route('/', method='DELETE') diff --git a/bin/views/highlight.html b/bin/views/highlight.html index 9ade7aa..bb9798f 100644 --- a/bin/views/highlight.html +++ b/bin/views/highlight.html @@ -6,6 +6,8 @@ + +