From e74bb06ce27a3ed603d1e17860d9cdb9c1d397b2 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Mon, 4 Mar 2024 22:41:38 +0100 Subject: [PATCH] Convert some pack files to Typescript --- app/javascript/packs/{share.jsx => share.tsx} | 8 +- app/javascript/packs/sign_up.js | 45 ---- app/javascript/packs/sign_up.ts | 48 +++++ .../packs/two_factor_authentication.js | 149 ------------- .../packs/two_factor_authentication.ts | 203 ++++++++++++++++++ 5 files changed, 256 insertions(+), 197 deletions(-) rename app/javascript/packs/{share.jsx => share.tsx} (84%) delete mode 100644 app/javascript/packs/sign_up.js create mode 100644 app/javascript/packs/sign_up.ts delete mode 100644 app/javascript/packs/two_factor_authentication.js create mode 100644 app/javascript/packs/two_factor_authentication.ts diff --git a/app/javascript/packs/share.jsx b/app/javascript/packs/share.tsx similarity index 84% rename from app/javascript/packs/share.jsx rename to app/javascript/packs/share.tsx index 74730d5e1a6..c68fc8fe865 100644 --- a/app/javascript/packs/share.jsx +++ b/app/javascript/packs/share.tsx @@ -16,7 +16,7 @@ function loaded() { if (!attr) return; - const props = JSON.parse(attr); + const props = JSON.parse(attr) as object; const root = createRoot(mountNode); root.render(); @@ -24,11 +24,13 @@ function loaded() { } function main() { - ready(loaded); + ready(loaded).catch((error) => { + throw error; + }); } loadPolyfills() .then(main) .catch((error) => { - console.error(error); + throw error; }); diff --git a/app/javascript/packs/sign_up.js b/app/javascript/packs/sign_up.js deleted file mode 100644 index 89acf15ddd4..00000000000 --- a/app/javascript/packs/sign_up.js +++ /dev/null @@ -1,45 +0,0 @@ -import './public-path'; -import axios from 'axios'; - -import ready from '../mastodon/ready'; - -ready(() => { - setInterval(() => { - axios - .get('/api/v1/emails/check_confirmation') - .then((response) => { - if (response.data) { - window.location = '/start'; - } - }) - .catch((error) => { - console.error(error); - }); - }, 5000); - - document.querySelectorAll('.timer-button').forEach((button) => { - let counter = 30; - - const container = document.createElement('span'); - - const updateCounter = () => { - container.innerText = ` (${counter})`; - }; - - updateCounter(); - - const countdown = setInterval(() => { - counter--; - - if (counter === 0) { - button.disabled = false; - button.removeChild(container); - clearInterval(countdown); - } else { - updateCounter(); - } - }, 1000); - - button.appendChild(container); - }); -}); diff --git a/app/javascript/packs/sign_up.ts b/app/javascript/packs/sign_up.ts new file mode 100644 index 00000000000..70d7c7aef35 --- /dev/null +++ b/app/javascript/packs/sign_up.ts @@ -0,0 +1,48 @@ +import './public-path'; +import axios from 'axios'; + +import ready from '../mastodon/ready'; + +async function checkConfirmation() { + const response = await axios.get('/api/v1/emails/check_confirmation'); + + if (response.data) { + window.location.href = '/start'; + } +} + +ready(() => { + setInterval(() => { + void checkConfirmation(); + }, 5000); + + document + .querySelectorAll('button.timer-button') + .forEach((button) => { + let counter = 30; + + const container = document.createElement('span'); + + const updateCounter = () => { + container.innerText = ` (${counter})`; + }; + + updateCounter(); + + const countdown = setInterval(() => { + counter--; + + if (counter === 0) { + button.disabled = false; + button.removeChild(container); + clearInterval(countdown); + } else { + updateCounter(); + } + }, 1000); + + button.appendChild(container); + }); +}).catch((e) => { + throw e; +}); diff --git a/app/javascript/packs/two_factor_authentication.js b/app/javascript/packs/two_factor_authentication.js deleted file mode 100644 index b95f417cd04..00000000000 --- a/app/javascript/packs/two_factor_authentication.js +++ /dev/null @@ -1,149 +0,0 @@ -import * as WebAuthnJSON from '@github/webauthn-json'; -import axios from 'axios'; - -import ready from '../mastodon/ready'; -import 'regenerator-runtime/runtime'; - -function getCSRFToken() { - var CSRFSelector = document.querySelector('meta[name="csrf-token"]'); - if (CSRFSelector) { - return CSRFSelector.getAttribute('content'); - } else { - return null; - } -} - -function hideFlashMessages() { - Array.from(document.getElementsByClassName('flash-message')).forEach( - function (flashMessage) { - flashMessage.classList.add('hidden'); - }, - ); -} - -function callback(url, body) { - axios - .post(url, JSON.stringify(body), { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-CSRF-Token': getCSRFToken(), - }, - credentials: 'same-origin', - }) - .then(function (response) { - window.location.replace(response.data.redirect_path); - }) - .catch(function (error) { - if (error.response.status === 422) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage.classList.remove('hidden'); - console.error(error.response.data.error); - } else { - console.error(error); - } - }); -} - -ready(() => { - if (!WebAuthnJSON.supported()) { - const unsupported_browser_message = document.getElementById( - 'unsupported-browser-message', - ); - if (unsupported_browser_message) { - unsupported_browser_message.classList.remove('hidden'); - document.querySelector('.btn.js-webauthn').disabled = true; - } - } - - const webAuthnCredentialRegistrationForm = document.getElementById( - 'new_webauthn_credential', - ); - if (webAuthnCredentialRegistrationForm) { - webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => { - event.preventDefault(); - - var nickname = event.target.querySelector( - 'input[name="new_webauthn_credential[nickname]"]', - ); - if (nickname.value) { - axios - .get('/settings/security_keys/options') - .then((response) => { - const credentialOptions = response.data; - - WebAuthnJSON.create({ publicKey: credentialOptions }) - .then((credential) => { - var params = { - credential: credential, - nickname: nickname.value, - }; - callback('/settings/security_keys', params); - }) - .catch((error) => { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage.classList.remove('hidden'); - console.error(error); - }); - }) - .catch((error) => { - console.error(error.response.data.error); - }); - } else { - nickname.focus(); - } - }); - } - - const webAuthnCredentialAuthenticationForm = - document.getElementById('webauthn-form'); - if (webAuthnCredentialAuthenticationForm) { - webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => { - event.preventDefault(); - - axios - .get('sessions/security_key_options') - .then((response) => { - const credentialOptions = response.data; - - WebAuthnJSON.get({ publicKey: credentialOptions }) - .then((credential) => { - var params = { user: { credential: credential } }; - callback('sign_in', params); - }) - .catch((error) => { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage.classList.remove('hidden'); - console.error(error); - }); - }) - .catch((error) => { - console.error(error.response.data.error); - }); - }); - - const otpAuthenticationForm = document.getElementById( - 'otp-authentication-form', - ); - - const linkToOtp = document.getElementById('link-to-otp'); - linkToOtp.addEventListener('click', () => { - webAuthnCredentialAuthenticationForm.classList.add('hidden'); - otpAuthenticationForm.classList.remove('hidden'); - hideFlashMessages(); - }); - - const linkToWebAuthn = document.getElementById('link-to-webauthn'); - linkToWebAuthn.addEventListener('click', () => { - otpAuthenticationForm.classList.add('hidden'); - webAuthnCredentialAuthenticationForm.classList.remove('hidden'); - hideFlashMessages(); - }); - } -}); diff --git a/app/javascript/packs/two_factor_authentication.ts b/app/javascript/packs/two_factor_authentication.ts new file mode 100644 index 00000000000..b8fa33c8940 --- /dev/null +++ b/app/javascript/packs/two_factor_authentication.ts @@ -0,0 +1,203 @@ +import * as WebAuthnJSON from '@github/webauthn-json'; +import type { PublicKeyCredentialCreationOptionsJSON } from '@github/webauthn-json/dist/types/basic/json'; +import axios, { AxiosError } from 'axios'; + +import ready from '../mastodon/ready'; + +import 'regenerator-runtime/runtime'; + +function exceptionHasAxiosError( + error: unknown, +): error is AxiosError<{ error: unknown }> { + return ( + error instanceof AxiosError && + typeof error.response?.data === 'object' && + 'error' in error.response.data + ); +} + +function logAxiosResponseError(error: unknown) { + if (exceptionHasAxiosError(error)) console.error(error); +} + +function getCSRFToken() { + const CSRFSelector = document.querySelector( + 'meta[name="csrf-token"]', + ); + if (CSRFSelector) { + return CSRFSelector.getAttribute('content'); + } else { + return null; + } +} + +function hideFlashMessages() { + Array.from(document.getElementsByClassName('flash-message')).forEach( + function (flashMessage) { + flashMessage.classList.add('hidden'); + }, + ); +} + +async function callback( + url: string, + body: + | { + credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON; + nickname: string; + } + | { + user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON }; + }, +) { + try { + const response = await axios.post<{ redirect_path: string }>( + url, + JSON.stringify(body), + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-CSRF-Token': getCSRFToken(), + }, + // credentials: 'same-origin', + }, + ); + + window.location.replace(response.data.redirect_path); + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 422) { + const errorMessage = document.getElementById( + 'security-key-error-message', + ); + errorMessage?.classList.remove('hidden'); + + logAxiosResponseError(error); + } else { + console.error(error); + } + } +} + +async function handleWebauthnCredentialRegistration(nickname: string) { + try { + const response = await axios.get( + '/settings/security_keys/options', + ); + + const credentialOptions = response.data; + + try { + const credential = await WebAuthnJSON.create({ + publicKey: credentialOptions, + }); + + const params = { + credential: credential, + nickname: nickname, + }; + + await callback('/settings/security_keys', params); + } catch (error) { + const errorMessage = document.getElementById( + 'security-key-error-message', + ); + errorMessage?.classList.remove('hidden'); + console.error(error); + } + } catch (error) { + logAxiosResponseError(error); + } +} + +async function handleWebauthnCredentialAuthentication() { + try { + const response = await axios.get( + 'sessions/security_key_options', + ); + + const credentialOptions = response.data; + + try { + const credential = await WebAuthnJSON.get({ + publicKey: credentialOptions, + }); + + const params = { user: { credential: credential } }; + void callback('sign_in', params); + } catch (error) { + const errorMessage = document.getElementById( + 'security-key-error-message', + ); + errorMessage?.classList.remove('hidden'); + console.error(error); + } + } catch (error) { + logAxiosResponseError(error); + } +} + +ready(() => { + if (!WebAuthnJSON.supported()) { + const unsupported_browser_message = document.getElementById( + 'unsupported-browser-message', + ); + if (unsupported_browser_message) { + unsupported_browser_message.classList.remove('hidden'); + const button = document.querySelector( + 'button.btn.js-webauthn', + ); + if (button) button.disabled = true; + } + } + + const webAuthnCredentialRegistrationForm = + document.querySelector('form#new_webauthn_credential'); + if (webAuthnCredentialRegistrationForm) { + webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => { + event.preventDefault(); + + if (!(event.target instanceof HTMLFormElement)) return; + + const nickname = event.target.querySelector( + 'input[name="new_webauthn_credential[nickname]"]', + ); + + if (nickname?.value) { + void handleWebauthnCredentialRegistration(nickname.value); + } else { + nickname?.focus(); + } + }); + } + + const webAuthnCredentialAuthenticationForm = + document.getElementById('webauthn-form'); + if (webAuthnCredentialAuthenticationForm) { + webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => { + event.preventDefault(); + void handleWebauthnCredentialAuthentication(); + }); + + const otpAuthenticationForm = document.getElementById( + 'otp-authentication-form', + ); + + const linkToOtp = document.getElementById('link-to-otp'); + + linkToOtp?.addEventListener('click', () => { + webAuthnCredentialAuthenticationForm.classList.add('hidden'); + otpAuthenticationForm?.classList.remove('hidden'); + hideFlashMessages(); + }); + + const linkToWebAuthn = document.getElementById('link-to-webauthn'); + linkToWebAuthn?.addEventListener('click', () => { + otpAuthenticationForm?.classList.add('hidden'); + webAuthnCredentialAuthenticationForm.classList.remove('hidden'); + hideFlashMessages(); + }); + } +}).catch((e: unknown) => { + throw e; +});