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 74730d5e1a..c68fc8fe86 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 89acf15ddd..0000000000
--- 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 0000000000..70d7c7aef3
--- /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 b95f417cd0..0000000000
--- 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 0000000000..b8fa33c894
--- /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;
+});