mirror of https://github.com/mastodon/mastodon
Merge 19cc4ca567
into 65093c619f
This commit is contained in:
commit
43b1269bed
|
@ -0,0 +1,15 @@
|
|||
import './public-path';
|
||||
import main from 'mastodon/main';
|
||||
|
||||
import { start } from '../mastodon/common';
|
||||
import { loadLocale } from '../mastodon/locales';
|
||||
import { loadPolyfills } from '../mastodon/polyfills';
|
||||
|
||||
start();
|
||||
|
||||
loadPolyfills()
|
||||
.then(loadLocale)
|
||||
.then(main)
|
||||
.catch((e: unknown) => {
|
||||
console.error(e);
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import './public-path';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
ready(() => {
|
||||
const image = document.querySelector<HTMLImageElement>('img');
|
||||
|
||||
if (!image) return;
|
||||
|
||||
image.addEventListener('mouseenter', () => {
|
||||
image.src = '/oops.gif';
|
||||
});
|
||||
|
||||
image.addEventListener('mouseleave', () => {
|
||||
image.src = '/oops.png';
|
||||
});
|
||||
}).catch((e: unknown) => {
|
||||
console.error(e);
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
/* Placeholder file to have `inert.scss` compiled by Webpack
|
||||
This is used by the `wicg-inert` polyfill */
|
||||
|
||||
import '../styles/inert.scss';
|
|
@ -0,0 +1,5 @@
|
|||
/* Placeholder to make the mailer assets compiled by Webpack */
|
||||
|
||||
import '../styles/mailer.scss';
|
||||
|
||||
require.context('../icons');
|
|
@ -0,0 +1,23 @@
|
|||
// Dynamically set webpack's loading path depending on a meta header, in order
|
||||
// to share the same assets regardless of instance configuration.
|
||||
// See https://webpack.js.org/guides/public-path/#on-the-fly
|
||||
|
||||
function removeOuterSlashes(string: string) {
|
||||
return string.replace(/^\/*/, '').replace(/\/*$/, '');
|
||||
}
|
||||
|
||||
function formatPublicPath(host = '', path = '') {
|
||||
let formattedHost = removeOuterSlashes(host);
|
||||
if (formattedHost && !/^http/i.test(formattedHost)) {
|
||||
formattedHost = `//${formattedHost}`;
|
||||
}
|
||||
const formattedPath = removeOuterSlashes(path);
|
||||
return `${formattedHost}/${formattedPath}/`;
|
||||
}
|
||||
|
||||
const cdnHost = document.querySelector<HTMLMetaElement>('meta[name=cdn-host]');
|
||||
|
||||
__webpack_public_path__ = formatPublicPath(
|
||||
cdnHost ? cdnHost.content : '',
|
||||
process.env.PUBLIC_OUTPUT_PATH,
|
||||
);
|
|
@ -0,0 +1,36 @@
|
|||
import './public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { start } from '../mastodon/common';
|
||||
import ComposeContainer from '../mastodon/containers/compose_container';
|
||||
import { loadPolyfills } from '../mastodon/polyfills';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
start();
|
||||
|
||||
function loaded() {
|
||||
const mountNode = document.getElementById('mastodon-compose');
|
||||
|
||||
if (mountNode) {
|
||||
const attr = mountNode.getAttribute('data-props');
|
||||
|
||||
if (!attr) return;
|
||||
|
||||
const props = JSON.parse(attr) as object;
|
||||
const root = createRoot(mountNode);
|
||||
|
||||
root.render(<ComposeContainer {...props} />);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
ready(loaded).catch((error: unknown) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills()
|
||||
.then(main)
|
||||
.catch((error: unknown) => {
|
||||
throw error;
|
||||
});
|
|
@ -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<HTMLButtonElement>('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: unknown) => {
|
||||
throw e;
|
||||
});
|
|
@ -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<HTMLMetaElement>(
|
||||
'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<PublicKeyCredentialCreationOptionsJSON>(
|
||||
'/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<PublicKeyCredentialCreationOptionsJSON>(
|
||||
'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<HTMLButtonElement>(
|
||||
'button.btn.js-webauthn',
|
||||
);
|
||||
if (button) button.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const webAuthnCredentialRegistrationForm =
|
||||
document.querySelector<HTMLFormElement>('form#new_webauthn_credential');
|
||||
if (webAuthnCredentialRegistrationForm) {
|
||||
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLFormElement)) return;
|
||||
|
||||
const nickname = event.target.querySelector<HTMLInputElement>(
|
||||
'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;
|
||||
});
|
|
@ -167,6 +167,7 @@
|
|||
"@types/redux-immutable": "^4.0.3",
|
||||
"@types/requestidlecallback": "^0.3.5",
|
||||
"@types/webpack": "^4.41.33",
|
||||
"@types/webpack-env": "^1.18.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
|
|
|
@ -2766,6 +2766,7 @@ __metadata:
|
|||
"@types/redux-immutable": "npm:^4.0.3"
|
||||
"@types/requestidlecallback": "npm:^0.3.5"
|
||||
"@types/webpack": "npm:^4.41.33"
|
||||
"@types/webpack-env": "npm:^1.18.4"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.0.0"
|
||||
"@typescript-eslint/parser": "npm:^7.0.0"
|
||||
arrow-key-navigation: "npm:^1.2.0"
|
||||
|
@ -3990,6 +3991,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webpack-env@npm:^1.18.4":
|
||||
version: 1.18.4
|
||||
resolution: "@types/webpack-env@npm:1.18.4"
|
||||
checksum: 10c0/3fa77dbff0ed71685404576b0a1cf74587567fe2ee1cfd11d56d6eefcab7a61e4c9ead0eced264e289d2cf0fc74296dbd55ed6c95774fe0fd6264d156c5a59f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webpack-sources@npm:*":
|
||||
version: 3.2.2
|
||||
resolution: "@types/webpack-sources@npm:3.2.2"
|
||||
|
|
Loading…
Reference in New Issue