mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
* 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>
1016 lines
30 KiB
Vue
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>
|