(() => { const elemPreviewEmail = $('#preview-email'), actionEmailReveal = $('#email-reveal'), actionEmailEdit = $('#email-edit'), actionEmailResend = $('#email-resend'), actionPassChange = $('#password-change'), actionMFASetup = $('#mfa-setup-start'), actionMFASetupRemove = $('#mfa-setup-remove'), actionMFARecoveryView = $('#mfa-recovery-view'), actionMFARecoveryRegen = $('#mfa-recovery-regen'), actionDangerDeleteAccount = $('#danger-deleteaccount'), errorMFA = $('#error-mfa'), errorEmail = $('#error-email'), errorDanger = $('#error-danger') SendRequest('GET', '/api/v1/users/@me', null, 1) .then(([_, body]) => { // Email let emailHidden = false let emailTemplate = elemPreviewEmail.textContent actionEmailReveal.onclick = () => { let newPreview if (emailHidden) { newPreview = body.email actionEmailReveal.textContent = 'Hide Email Address' } else { const usernameLength = body.email.indexOf('@') const censoredSegment = ''.padStart(usernameLength > 12 ? 12 : usernameLength, '*') const hostSegment = body.email.slice(usernameLength) newPreview = censoredSegment + hostSegment actionEmailReveal.textContent = 'Reveal Email Address' } elemPreviewEmail.textContent = (emailTemplate + newPreview) emailHidden = !emailHidden } actionEmailReveal.onclick() if (!body.verified) { actionEmailResend.removeAttribute('hidden') actionEmailResend.onclick = () => { const unlock = LockInteraction() SendRequest('POST', '/api/v1/users/@me/security/email', null, 1) .then(() => CreateModal({ behaviour: { cancelText: '', submitText: 'Close', title: 'Verification Email Sent', tooltip: 'An email to verify this email address has been sent. Please check your spam or inbox.', } })) .catch(e => errorEmail.textContent = e) .finally(unlock) } } actionEmailEdit.onclick = () => { CreateModal({ behaviour: { title: 'Update Email', tooltip: 'Changing your email address will mark it as unverified.', onSubmit: ({ email }) => new Promise(ok => { SendRequest('PATCH', '/api/v1/users/@me/security/email', { email }, 2) .then(() => window.location.reload()) .catch(ok) }) }, '_divider': { type: 'divider-line' }, 'email': { type: 'text', required: true, title: 'Email Address', validator: TestEmailSuite, } }) } // Password actionPassChange.onclick = () => { CreateModal({ behaviour: { title: 'Change Password', tooltip: 'Enter your current password and new password.', onSubmit: ({ old_password, new_password }) => new Promise(ok => { SendRequest( 'PATCH', '/api/v1/users/@me/security/password', { old_password, new_password }, 1 ) .then(() => ok()) .catch(ok) }) }, '_link': { type: 'link', title: 'Forgot Password?', href: '/password-reset', }, '_divider': { type: 'divider-line' }, 'old_password': { type: 'password', required: true, title: 'Old Password', }, 'new_password': { type: 'password', required: true, title: 'New Password', validator: (v, { new_password_again }) => { if (v !== new_password_again) return 'Passwords do not Match' return TestPasswordSuite(v) }, }, 'new_password_again': { type: 'password', required: true, title: 'New Password Again', validator: (v, { new_password }) => { if (v !== new_password) return 'Passwords do not Match' return TestPasswordSuite(v) }, }, }) } // Multi-Factor function RecoveryModal(codes) { return CreateModal({ behaviour: { title: 'Recovery Codes', tooltip: 'Keep these recovery codes safe! ' + 'You\'ll lose access to your account if both recovery codes and your authenticator app are lost.', submitText: 'Download', cancelText: 'Close', onSubmit: () => { // Generate Text const filename = `Recovery Codes for ${body.username}.txt` const filedata = `Recovery Codes for ${body.username} (${body.email})\n` + `Generated on ${new Date().toLocaleString()}\n` + `------------\n` + codes.map(c => `> ${c}\n`).join(''); // Fake Download Element const a = document.createElement('a') a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(filedata)) a.setAttribute('download', filename) a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) return ' ' } }, '_codes': { type: 'recovery-codes', codes: codes } }) } if (body.mfa_enabled) { actionMFASetupRemove.removeAttribute('hidden') actionMFASetupRemove.onclick = async () => { await CreateModal({ behaviour: { title: 'Remove Authenticator App?', tooltip: 'This will disable MFA on your account.', } }) const unlock = LockInteraction() SendRequest('DELETE', '/api/v1/users/@me/security/mfa/setup', null, 2) .then(() => window.location.reload()) .catch(e => errorMFA.textContent = e) .finally(unlock) } actionMFARecoveryView.removeAttribute('hidden') actionMFARecoveryView.onclick = () => { const unlock = LockInteraction() SendRequest('GET', '/api/v1/users/@me/security/mfa/codes', null, 2) .then(([_, codes]) => RecoveryModal(codes)) .catch(e => errorMFA.textContent = e) .finally(unlock) } actionMFARecoveryRegen.removeAttribute('hidden') actionMFARecoveryRegen.onclick = async () => { await CreateModal({ behaviour: { title: 'Reset Recovery Codes?', tooltip: 'This will void the previously generated codes.', } }) const unlock = LockInteraction() SendRequest('DELETE', '/api/v1/users/@me/security/mfa/codes', null, 2) .then(([_, codes]) => RecoveryModal(codes)) .catch(e => errorMFA.textContent = e) .finally(unlock) } } else { actionMFASetup.removeAttribute('hidden') actionMFASetup.onclick = () => { const unlock = LockInteraction() SendRequest('GET', '/api/v1/users/@me/security/mfa/setup', null, 1) .then(([_, mfa]) => { CreateModal({ behaviour: { title: 'Setup Authenticator App', tooltip: 'Scan the QR code using your Authenticator App and enter the generated passcode.', onSubmit: ({ passcode }) => new Promise(ok => { const unlock = LockInteraction() SendRequest('POST', '/api/v1/users/@me/security/mfa/setup', { passcode }, 1) .then((/* 204 No Content */) => { RecoveryModal(mfa.recovery_codes) .finally(() => window.location.reload()) ok() }) .catch(ok) .finally(unlock) }) }, '_spacer1': { type: 'divider-line' }, '_qrcode': { type: 'qr-code', src: mfa.image, }, '_spacer2': { type: 'divider-line' }, '_secret': { type: 'text', title: 'Can\'t Scan the QR Code? Use this secret key:', value: mfa.secret, readonly: true, }, passcode: { required: true, type: 'password', title: 'Passcode', autocomplete: 'one-time-code', placeholder: '123456', validator: v => { if (!/[0-9]{6}/.test(v)) return 'Invalid Passcode' return true } }, }) }) .catch(e => errorMFA.textContent = e) .finally(unlock) } } // Danger Zone actionDangerDeleteAccount.onclick = async () => { // Prompt Modal await CreateModal({ behaviour: { title: 'Delete Account?', tooltip: 'This will start a countdown where afterwards your account and all of its data will be deleted. ' + 'More details will be sent to your email address.', } }) const unlock = LockInteraction() SendRequest('DELETE', '/api/v1/users/@me', null, 2) .then(() => SignOut()) .catch(e => errorDanger.textContent = e) .finally(unlock) } // Display Menu $('#security-wrapper').removeAttribute('hidden') }) .catch(e => { // Display Error Message $('#alert-error').removeAttribute('hidden') $('#error-message').textContent = e console.log(e) }) .finally(() => { // Hide Loading Symbol $('#alert-load').setAttribute('hidden', true) }) })()