Files
modrinth/apps/frontend/src/pages/settings/account.vue
T
ProspectorandCalum H. 7dbbbe590f chore: clean up a bunch of legacy styles (#5973)
* remove unused experimental-styles-within

* remove unused styles

* more cleanup + prepr

* Refactor nearly all legacy buttons to use ButtonStyled

* prepr

* Update MC account selector to modern version

* prepr

---------

Co-authored-by: Calum H. <calum@modrinth.com>
2026-05-03 18:53:06 +00:00

1016 lines
30 KiB
Vue

<template>
<div>
<ConfirmModal
ref="modal_confirm"
:title="formatMessage(messages.deleteAccountConfirmTitle)"
:description="formatMessage(messages.deleteAccountConfirmDescription)"
:proceed-label="formatMessage(messages.deleteAccountConfirmProceed)"
:confirmation-text="auth.user.username"
:has-to-type="true"
@proceed="deleteAccount"
/>
<Modal
ref="changeEmailModal"
:header="`${auth.user.email ? formatMessage(messages.changeEmailHeaderChange) : formatMessage(messages.changeEmailHeaderAdd)}`"
>
<div class="universal-modal">
<p>{{ formatMessage(messages.emailNotPublicNotice) }}</p>
<label for="email-input">
<span class="label__title">{{ formatMessage(messages.emailAddressLabel) }}</span>
</label>
<StyledInput
id="email-input"
v-model="email"
:maxlength="2048"
type="email"
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
@keyup.enter="saveEmail()"
/>
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.changeEmailModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!email" @click="saveEmail()">
<SaveIcon />
{{ formatMessage(messages.saveEmailButton) }}
</button>
</ButtonStyled>
</div>
</div>
</Modal>
<Modal
ref="managePasswordModal"
:header="`${
removePasswordMode
? formatMessage(messages.passwordHeaderRemove)
: auth.user.has_password
? formatMessage(messages.passwordHeaderChange)
: formatMessage(messages.passwordHeaderAdd)
}`"
>
<div class="universal-modal">
<ul
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
class="known-errors"
>
<li>{{ formatMessage(messages.passwordsDoNotMatchError) }}</li>
</ul>
<label v-if="removePasswordMode" for="old-password">
<span class="label__title">{{ formatMessage(messages.confirmPasswordLabel) }}</span>
<span class="label__description">{{
formatMessage(messages.confirmPasswordDescription)
}}</span>
</label>
<label v-else-if="auth.user.has_password" for="old-password">
<span class="label__title">{{ formatMessage(messages.oldPasswordLabel) }}</span>
</label>
<StyledInput
v-if="auth.user.has_password"
id="old-password"
v-model="oldPassword"
:maxlength="2048"
type="password"
autocomplete="current-password"
:placeholder="
removePasswordMode
? formatMessage(messages.confirmPasswordPlaceholder)
: formatMessage(messages.oldPasswordPlaceholder)
"
/>
<template v-if="!removePasswordMode">
<label for="new-password"
><span class="label__title">{{ formatMessage(messages.newPasswordLabel) }}</span></label
>
<StyledInput
id="new-password"
v-model="newPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
/>
<label for="confirm-new-password">
<span class="label__title">{{ formatMessage(messages.confirmNewPasswordLabel) }}</span>
</label>
<StyledInput
id="confirm-new-password"
v-model="confirmNewPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
/>
</template>
<p></p>
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.managePasswordModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<template v-if="removePasswordMode">
<ButtonStyled color="red">
<button :disabled="!oldPassword" @click="savePassword">
<TrashIcon />
{{ formatMessage(messages.removePasswordButton) }}
</button>
</ButtonStyled>
</template>
<template v-else>
<ButtonStyled
v-if="auth.user.has_password && auth.user.auth_providers.length > 0"
color="red"
>
<button @click="removePasswordMode = true">
<TrashIcon />
{{ formatMessage(messages.removePasswordButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button
:disabled="
newPassword.length == 0 ||
(auth.user.has_password && oldPassword.length == 0) ||
newPassword !== confirmNewPassword
"
@click="savePassword"
>
<SaveIcon />
{{ formatMessage(messages.savePasswordButton) }}
</button>
</ButtonStyled>
</template>
</div>
</div>
</Modal>
<Modal
ref="manageTwoFactorModal"
:header="`${auth.user.has_totp && twoFactorStep === 0 ? formatMessage(messages.twoFactorRemoveButton) : formatMessage(messages.twoFactorSetupButton)}`"
>
<div class="universal-modal">
<template v-if="auth.user.has_totp && twoFactorStep === 0">
<label for="two-factor-code">
<span class="label__title">{{ formatMessage(messages.twoFactorEnterCodeLabel) }}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorEnterCodeDescription)
}}</span>
</label>
<StyledInput
id="two-factor-code"
v-model="twoFactorCode"
:maxlength="11"
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="removeTwoFactor()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.manageTwoFactorModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="removeTwoFactor">
<TrashIcon />
{{ formatMessage(messages.twoFactorRemoveButton) }}
</button>
</ButtonStyled>
</div>
</template>
<template v-else>
<template v-if="twoFactorStep === 0">
<p>{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
<p>
<IntlFormatted :message-id="messages.twoFactorSetupScan">
<template #authy-link="{ children }">
<a href="https://authy.com/" target="_blank" rel="noreferrer">
<component :is="() => children" />
</a>
</template>
<template #microsoft-authenticator-link="{ children }">
<a
href="https://www.microsoft.com/en-us/security/mobile-authenticator-app"
target="_blank"
rel="noreferrer"
>
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</p>
<qrcode-vue
v-if="twoFactorSecret"
:value="`otpauth://totp/${encodeURIComponent(
auth.user.email,
)}?secret=${twoFactorSecret}&issuer=Modrinth`"
:size="250"
:margin="2"
level="H"
/>
<p>
{{ formatMessage(messages.twoFactorManualSecretPrefix) }}
<strong>{{ twoFactorSecret }}</strong>
</p>
</template>
<template v-if="twoFactorStep === 1">
<label for="verify-code">
<span class="label__title">{{
formatMessage(messages.twoFactorVerifyCodeLabel)
}}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorVerifyCodeDescription)
}}</span>
</label>
<StyledInput
id="verify-code"
v-model="twoFactorCode"
:maxlength="6"
autocomplete="one-time-code"
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="verifyTwoFactorCode()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
</template>
<template v-if="twoFactorStep === 2">
<p>{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
<p>{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
<ul>
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
</ul>
</template>
<div class="input-group push-right">
<ButtonStyled v-if="twoFactorStep === 1">
<button @click="twoFactorStep = 0">
<LeftArrowIcon />
{{ formatMessage(commonMessages.backButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="twoFactorStep !== 2">
<button @click="$refs.manageTwoFactorModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="twoFactorStep <= 1" color="brand">
<button @click="twoFactorStep === 1 ? verifyTwoFactorCode() : (twoFactorStep = 1)">
<RightArrowIcon />
{{ formatMessage(commonMessages.continueButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="twoFactorStep === 2" color="brand">
<button @click="$refs.manageTwoFactorModal.hide()">
<CheckIcon />
{{ formatMessage(messages.completeSetupButton) }}
</button>
</ButtonStyled>
</div>
</template>
</div>
</Modal>
<Modal ref="manageProvidersModal" :header="formatMessage(messages.manageProvidersModalHeader)">
<div class="universal-modal">
<div class="table">
<div class="table-head table-row">
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableProvider) }}
</div>
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableActions) }}
</div>
</div>
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
<div class="table-text table-cell">
<span><component :is="provider.icon" /> {{ provider.display }}</span>
</div>
<div class="table-text manage table-cell">
<ButtonStyled v-if="auth.user.auth_providers.includes(provider.id)">
<button @click="handleRemoveAuthProvider(provider.id)">
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<a :href="`${getAuthUrl(provider.id, '/settings/account')}&token=${auth.token}`">
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
</a>
</ButtonStyled>
</div>
</div>
</div>
<p></p>
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.manageProvidersModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.closeButton) }}
</button>
</ButtonStyled>
</div>
</div>
</Modal>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(messages.accountSecurityTitle) }}</h2>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">{{ formatMessage(messages.emailFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.emailFieldDescription)
}}</span>
</label>
<div>
<ButtonStyled>
<button @click="$refs.changeEmailModal.show()">
<template v-if="auth.user.email">
<EditIcon />
{{ formatMessage(messages.changeEmailButton) }}
</template>
<template v-else>
<PlusIcon />
{{ formatMessage(messages.addEmailButton) }}
</template>
</button>
</ButtonStyled>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">{{ formatMessage(messages.passwordFieldTitle) }}</span>
<span v-if="auth.user.has_password" class="label__description">
{{
auth.user.auth_providers.length > 0
? formatMessage(messages.passwordDescriptionChangeOrRemove)
: formatMessage(messages.passwordDescriptionChange)
}}
</span>
<span v-else class="label__description">
{{ formatMessage(messages.passwordDescriptionSet) }}
</span>
</label>
<div>
<ButtonStyled>
<button
@click="
() => {
oldPassword = ''
newPassword = ''
confirmNewPassword = ''
removePasswordMode = false
$refs.managePasswordModal.show()
}
"
>
<KeyIcon />
<template v-if="auth.user.has_password">{{
formatMessage(messages.changePasswordButton)
}}</template>
<template v-else> {{ formatMessage(messages.addPasswordButton) }} </template>
</button>
</ButtonStyled>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">{{ formatMessage(messages.twoFactorFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorFieldDescription)
}}</span>
</label>
<div>
<ButtonStyled>
<button @click="showTwoFactorModal">
<template v-if="auth.user.has_totp">
<TrashIcon /> {{ formatMessage(messages.twoFactorRemoveButton) }}
</template>
<template v-else>
<PlusIcon /> {{ formatMessage(messages.twoFactorSetupButton) }}
</template>
</button>
</ButtonStyled>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">{{ formatMessage(messages.manageProvidersFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.manageProvidersFieldDescription)
}}</span>
</label>
<div>
<ButtonStyled>
<button @click="$refs.manageProvidersModal.show()">
<SettingsIcon /> {{ formatMessage(messages.manageProvidersButton) }}
</button>
</ButtonStyled>
</div>
</div>
</section>
<section id="data-export" class="universal-card">
<h2>{{ formatMessage(messages.dataExportTitle) }}</h2>
<p>{{ formatMessage(messages.dataExportDescription) }}</p>
<ButtonStyled v-if="generated">
<a :href="generated" download="export.json">
<DownloadIcon />
{{ formatMessage(messages.downloadExportButton) }}
</a>
</ButtonStyled>
<ButtonStyled v-else>
<button :disabled="generatingExport" @click="exportData">
<template v-if="generatingExport">
<UpdatedIcon /> {{ formatMessage(messages.generatingExportButton) }}
</template>
<template v-else>
<UpdatedIcon /> {{ formatMessage(messages.generateExportButton) }}
</template>
</button>
</ButtonStyled>
</section>
<section id="delete-account" class="universal-card">
<h2>{{ formatMessage(messages.deleteAccountSectionTitle) }}</h2>
<p>{{ formatMessage(messages.deleteAccountSectionDescription) }}</p>
<ButtonStyled color="red">
<button type="button" @click="$refs.modal_confirm.show()">
<TrashIcon />
{{ formatMessage(messages.deleteAccountButton) }}
</button>
</ButtonStyled>
</section>
</div>
</template>
<script setup>
import {
CheckIcon,
DownloadIcon,
EditIcon,
ExternalIcon,
LeftArrowIcon,
PlusIcon,
RightArrowIcon,
SaveIcon,
SettingsIcon,
TrashIcon,
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
ConfirmModal,
defineMessages,
injectNotificationManager,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import KeyIcon from 'assets/icons/auth/key.svg'
import DiscordIcon from 'assets/icons/auth/sso-discord.svg'
import GithubIcon from 'assets/icons/auth/sso-github.svg'
import GitLabIcon from 'assets/icons/auth/sso-gitlab.svg'
import GoogleIcon from 'assets/icons/auth/sso-google.svg'
import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
import QrcodeVue from 'qrcode.vue'
import Modal from '~/components/ui/Modal.vue'
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.js'
definePageMeta({
middleware: 'auth',
})
const { addNotification } = injectNotificationManager()
const auth = await useAuth()
const { formatMessage } = useVIntl()
const messages = defineMessages({
deleteAccountConfirmTitle: {
id: 'settings.account.delete.confirm.title',
defaultMessage: 'Are you sure you want to delete your account?',
},
deleteAccountConfirmDescription: {
id: 'settings.account.delete.confirm.description',
defaultMessage:
'This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.modrinth.com).',
},
deleteAccountConfirmProceed: {
id: 'settings.account.delete.confirm.proceed',
defaultMessage: 'Delete this account',
},
changeEmailHeaderChange: {
id: 'settings.account.email.modal.header.change',
defaultMessage: 'Change email',
},
changeEmailHeaderAdd: {
id: 'settings.account.email.modal.header.add',
defaultMessage: 'Add email',
},
emailNotPublicNotice: {
id: 'settings.account.email.modal.notice',
defaultMessage: 'Your account information is not displayed publicly.',
},
emailAddressLabel: {
id: 'settings.account.email.field.label',
defaultMessage: 'Email address',
},
emailAddressPlaceholder: {
id: 'settings.account.email.field.placeholder',
defaultMessage: 'Enter your email address...',
},
saveEmailButton: {
id: 'settings.account.email.action.save',
defaultMessage: 'Save email',
},
passwordHeaderRemove: {
id: 'settings.account.password.modal.header.remove',
defaultMessage: 'Remove password',
},
passwordHeaderChange: {
id: 'settings.account.password.modal.header.change',
defaultMessage: 'Change password',
},
passwordHeaderAdd: {
id: 'settings.account.password.modal.header.add',
defaultMessage: 'Add password',
},
passwordsDoNotMatchError: {
id: 'settings.account.password.error.mismatch',
defaultMessage: 'Input passwords do not match!',
},
confirmPasswordLabel: {
id: 'settings.account.password.field.confirm-current.label',
defaultMessage: 'Confirm password',
},
confirmPasswordDescription: {
id: 'settings.account.password.field.confirm-current.description',
defaultMessage: 'Please enter your password to proceed.',
},
oldPasswordLabel: {
id: 'settings.account.password.field.old.label',
defaultMessage: 'Old password',
},
confirmPasswordPlaceholder: {
id: 'settings.account.password.field.confirm-current.placeholder',
defaultMessage: 'Confirm password',
},
oldPasswordPlaceholder: {
id: 'settings.account.password.field.old.placeholder',
defaultMessage: 'Old password',
},
newPasswordLabel: {
id: 'settings.account.password.field.new.label',
defaultMessage: 'New password',
},
newPasswordPlaceholder: {
id: 'settings.account.password.field.new.placeholder',
defaultMessage: 'New password',
},
confirmNewPasswordLabel: {
id: 'settings.account.password.field.confirm-new.label',
defaultMessage: 'Confirm new password',
},
confirmNewPasswordPlaceholder: {
id: 'settings.account.password.field.confirm-new.placeholder',
defaultMessage: 'Confirm new password',
},
removePasswordButton: {
id: 'settings.account.password.action.remove',
defaultMessage: 'Remove password',
},
savePasswordButton: {
id: 'settings.account.password.action.save',
defaultMessage: 'Save password',
},
accountSecurityTitle: {
id: 'settings.account.security.title',
defaultMessage: 'Account security',
},
emailFieldTitle: {
id: 'settings.account.security.email.title',
defaultMessage: 'Email',
},
emailFieldDescription: {
id: 'settings.account.security.email.description',
defaultMessage: 'Changes the email associated with your account.',
},
changeEmailButton: {
id: 'settings.account.security.email.action.change',
defaultMessage: 'Change email',
},
addEmailButton: {
id: 'settings.account.security.email.action.add',
defaultMessage: 'Add email',
},
passwordFieldTitle: {
id: 'settings.account.security.password.title',
defaultMessage: 'Password',
},
passwordDescriptionChange: {
id: 'settings.account.security.password.description.change',
defaultMessage: 'Change the password used to login to your account.',
},
passwordDescriptionChangeOrRemove: {
id: 'settings.account.security.password.description.change-or-remove',
defaultMessage: 'Change or remove the password used to login to your account.',
},
passwordDescriptionSet: {
id: 'settings.account.security.password.description.set',
defaultMessage: 'Set a permanent password to login to your account.',
},
changePasswordButton: {
id: 'settings.account.security.password.action.change',
defaultMessage: 'Change password',
},
addPasswordButton: {
id: 'settings.account.security.password.action.add',
defaultMessage: 'Add password',
},
twoFactorFieldTitle: {
id: 'settings.account.security.two-factor.title',
defaultMessage: 'Two-factor authentication',
},
twoFactorFieldDescription: {
id: 'settings.account.security.two-factor.description',
defaultMessage: 'Add an additional layer of security to your account during login.',
},
twoFactorSetupButton: {
id: 'settings.account.security.two-factor.action.setup',
defaultMessage: 'Setup 2FA',
},
twoFactorEnterCodeLabel: {
id: 'settings.account.two-factor.field.code.label',
defaultMessage: 'Enter two-factor code',
},
twoFactorEnterCodeDescription: {
id: 'settings.account.two-factor.field.code.description',
defaultMessage: 'Please enter a two-factor code to proceed.',
},
twoFactorCodePlaceholder: {
id: 'settings.account.two-factor.field.code.placeholder',
defaultMessage: 'Enter code...',
},
twoFactorIncorrectError: {
id: 'settings.account.two-factor.error.incorrect-code',
defaultMessage: 'The code entered is incorrect!',
},
twoFactorRemoveButton: {
id: 'settings.account.security.two-factor.action.remove',
defaultMessage: 'Remove 2FA',
},
twoFactorSetupIntro: {
id: 'settings.account.two-factor.setup.intro',
defaultMessage:
'Two-factor authentication keeps your account secure by requiring access to a second device in order to sign in.',
},
twoFactorSetupScan: {
id: 'settings.account.two-factor.setup.scan',
defaultMessage:
'Scan the QR code with <authy-link>Authy</authy-link>, <microsoft-authenticator-link>Microsoft Authenticator</microsoft-authenticator-link>, or any other 2FA app to begin.',
},
twoFactorManualSecretPrefix: {
id: 'settings.account.two-factor.setup.manual-secret',
defaultMessage: 'If the QR code does not scan, you can manually enter the secret:',
},
twoFactorVerifyCodeLabel: {
id: 'settings.account.two-factor.verify.label',
defaultMessage: 'Verify code',
},
twoFactorVerifyCodeDescription: {
id: 'settings.account.two-factor.verify.description',
defaultMessage: 'Enter the one-time code from authenticator to verify access.',
},
twoFactorBackupCodesIntro: {
id: 'settings.account.two-factor.backup.intro',
defaultMessage:
'Download and save these back-up codes in a safe place. You can use these in-place of a 2FA code if you ever lose access to your device! You should protect these codes like your password.',
},
twoFactorBackupCodesSingleUse: {
id: 'settings.account.two-factor.backup.single-use',
defaultMessage: 'Backup codes can only be used once.',
},
completeSetupButton: {
id: 'settings.account.button.complete-setup',
defaultMessage: 'Complete setup',
},
manageProvidersModalHeader: {
id: 'settings.account.providers.modal.header',
defaultMessage: 'Authentication providers',
},
providersTableProvider: {
id: 'settings.account.providers.table.provider',
defaultMessage: 'Provider',
},
providersTableActions: {
id: 'settings.account.providers.table.actions',
defaultMessage: 'Actions',
},
providerAddButton: {
id: 'settings.account.providers.action.add',
defaultMessage: 'Add',
},
manageProvidersFieldTitle: {
id: 'settings.account.security.providers.title',
defaultMessage: 'Manage authentication providers',
},
manageProvidersFieldDescription: {
id: 'settings.account.security.providers.description',
defaultMessage:
'Add or remove sign-on methods from your account, including GitHub, GitLab, Microsoft, Discord, Steam, and Google.',
},
manageProvidersButton: {
id: 'settings.account.security.providers.action.manage',
defaultMessage: 'Manage providers',
},
dataExportTitle: {
id: 'settings.account.data-export.title',
defaultMessage: 'Data export',
},
dataExportDescription: {
id: 'settings.account.data-export.description',
defaultMessage:
'Request a copy of all your personal data you have uploaded to Modrinth. This may take several minutes to complete.',
},
downloadExportButton: {
id: 'settings.account.data-export.action.download',
defaultMessage: 'Download export',
},
generatingExportButton: {
id: 'settings.account.data-export.action.generating',
defaultMessage: 'Generating export...',
},
generateExportButton: {
id: 'settings.account.data-export.action.generate',
defaultMessage: 'Generate export',
},
deleteAccountSectionTitle: {
id: 'settings.account.delete.section.title',
defaultMessage: 'Delete account',
},
deleteAccountSectionDescription: {
id: 'settings.account.delete.section.description',
defaultMessage:
'Once you delete your account, there is no going back. Deleting your account will remove all attached data, excluding projects, from our servers.',
},
deleteAccountButton: {
id: 'settings.account.delete.section.action',
defaultMessage: 'Delete account',
},
})
const changeEmailModal = ref()
const email = ref(auth.value.user.email)
async function saveEmail() {
if (!email.value) {
return
}
startLoading()
try {
await useBaseFetch(`auth/email`, {
method: 'PATCH',
body: {
email: email.value,
},
})
changeEmailModal.value.hide()
await useAuth(auth.value.token)
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
async function handleRemoveAuthProvider(provider) {
try {
await removeAuthProvider(provider)
} catch (error) {
handleError(error)
}
}
const managePasswordModal = ref()
const removePasswordMode = ref(false)
const oldPassword = ref('')
const newPassword = ref('')
const confirmNewPassword = ref('')
async function savePassword() {
if (newPassword.value !== confirmNewPassword.value) {
return
}
startLoading()
try {
await useBaseFetch(`auth/password`, {
method: 'PATCH',
body: {
old_password: auth.value.user.has_password ? oldPassword.value : null,
new_password: removePasswordMode.value ? null : newPassword.value,
},
})
managePasswordModal.value.hide()
await useAuth(auth.value.token)
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
const manageTwoFactorModal = ref()
const twoFactorSecret = ref(null)
const twoFactorFlow = ref(null)
const twoFactorStep = ref(0)
async function showTwoFactorModal() {
twoFactorStep.value = 0
twoFactorCode.value = null
twoFactorIncorrect.value = false
if (auth.value.user.has_totp) {
manageTwoFactorModal.value.show()
return
}
twoFactorSecret.value = null
twoFactorFlow.value = null
backupCodes.value = []
manageTwoFactorModal.value.show()
startLoading()
try {
const res = await useBaseFetch('auth/2fa/get_secret', {
method: 'POST',
})
twoFactorSecret.value = res.secret
twoFactorFlow.value = res.flow
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
const twoFactorIncorrect = ref(false)
const twoFactorCode = ref(null)
const backupCodes = ref([])
async function verifyTwoFactorCode() {
startLoading()
try {
const res = await useBaseFetch('auth/2fa', {
method: 'POST',
body: {
code: twoFactorCode.value ? twoFactorCode.value : '',
flow: twoFactorFlow.value,
},
})
backupCodes.value = res.backup_codes
twoFactorStep.value = 2
await useAuth(auth.value.token)
} catch {
twoFactorIncorrect.value = true
}
stopLoading()
}
async function removeTwoFactor() {
startLoading()
try {
await useBaseFetch('auth/2fa', {
method: 'DELETE',
body: {
code: twoFactorCode.value ? twoFactorCode.value.toString() : '',
},
})
manageTwoFactorModal.value.hide()
await useAuth(auth.value.token)
} catch {
twoFactorIncorrect.value = true
}
stopLoading()
}
const authProviders = [
{
id: 'github',
display: 'GitHub',
icon: GithubIcon,
},
{
id: 'gitlab',
display: 'GitLab',
icon: GitLabIcon,
},
{
id: 'steam',
display: 'Steam',
icon: SteamIcon,
},
{
id: 'discord',
display: 'Discord',
icon: DiscordIcon,
},
{
id: 'microsoft',
display: 'Microsoft',
icon: MicrosoftIcon,
},
{
id: 'google',
display: 'Google',
icon: GoogleIcon,
},
]
async function deleteAccount() {
startLoading()
try {
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'DELETE',
})
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
}
useCookie('auth-token').value = null
window.location.href = '/'
stopLoading()
}
const generatingExport = ref(false)
const generated = ref()
async function exportData() {
startLoading()
generatingExport.value = true
try {
const res = await useBaseFetch('gdpr/export', {
method: 'POST',
internal: true,
})
const jsonString = JSON.stringify(res, null, 2)
const blob = new Blob([jsonString], { type: 'application/json' })
generated.value = URL.createObjectURL(blob)
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
}
generatingExport.value = false
stopLoading()
}
</script>
<style lang="scss" scoped>
canvas {
margin: 0 auto;
border-radius: var(--size-rounded-card);
}
.table-row {
grid-template-columns: 1fr 10rem;
span {
display: flex;
align-items: center;
margin: auto 0;
svg {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.35rem;
}
}
}
</style>