diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue index 10599d99ff..9ebf5101cd 100644 --- a/packages/client/src/pages/settings/2fa.vue +++ b/packages/client/src/pages/settings/2fa.vue @@ -1,49 +1,49 @@ <template> <div> - <MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton> + <MkButton v-if="!twoFactorData && !$i.twoFactorEnabled" @click="register">{{ i18n.ts._2fa.registerDevice }}</MkButton> <template v-if="$i.twoFactorEnabled"> - <p>{{ $ts._2fa.alreadyRegistered }}</p> - <MkButton @click="unregister">{{ $ts.unregister }}</MkButton> + <p>{{ i18n.ts._2fa.alreadyRegistered }}</p> + <MkButton @click="unregister">{{ i18n.ts.unregister }}</MkButton> <template v-if="supportsCredentials"> <hr class="totp-method-sep"> - <h2 class="heading">{{ $ts.securityKey }}</h2> - <p>{{ $ts._2fa.securityKeyInfo }}</p> + <h2 class="heading">{{ i18n.ts.securityKey }}</h2> + <p>{{ i18n.ts._2fa.securityKeyInfo }}</p> <div class="key-list"> <div v-for="key in $i.securityKeysList" class="key"> <h3>{{ key.name }}</h3> - <div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div> - <MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton> + <div class="last-used">{{ i18n.ts.lastUsed }}<MkTime :time="key.lastUsed"/></div> + <MkButton @click="unregisterKey(key)">{{ i18n.ts.unregister }}</MkButton> </div> </div> - <MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ $ts.passwordLessLogin }}</MkSwitch> + <MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ i18n.ts.passwordLessLogin }}</MkSwitch> - <MkInfo v-if="registration && registration.error" warn>{{ $ts.error }} {{ registration.error }}</MkInfo> - <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton> + <MkInfo v-if="registration && registration.error" warn>{{ i18n.ts.error }} {{ registration.error }}</MkInfo> + <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ i18n.ts._2fa.registerKey }}</MkButton> <ol v-if="registration && !registration.error"> <li v-if="registration.stage >= 0"> - {{ $ts.tapSecurityKey }} + {{ i18n.ts.tapSecurityKey }} <i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i> </li> <li v-if="registration.stage >= 1"> <MkForm :disabled="registration.stage != 1 || registration.saving"> <MkInput v-model="keyName" :max="30"> - <template #label>{{ $ts.securityKeyName }}</template> + <template #label>{{ i18n.ts.securityKeyName }}</template> </MkInput> - <MkButton :disabled="keyName.length == 0" @click="registerKey">{{ $ts.registerSecurityKey }}</MkButton> + <MkButton :disabled="keyName.length == 0" @click="registerKey">{{ i18n.ts.registerSecurityKey }}</MkButton> <i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i> </MkForm> </li> </ol> </template> </template> - <div v-if="data && !$i.twoFactorEnabled"> + <div v-if="twoFactorData && !$i.twoFactorEnabled"> <ol style="margin: 0; padding: 0 0 0 1em;"> <li> - <I18n :src="$ts._2fa.step1" tag="span"> + <I18n :src="i18n.ts._2fa.step1" tag="span"> <template #a> <a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a> </template> @@ -52,19 +52,19 @@ </template> </I18n> </li> - <li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li> - <li>{{ $ts._2fa.step3 }}<br> - <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput> - <MkButton primary @click="submit">{{ $ts.done }}</MkButton> + <li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"></li> + <li>{{ i18n.ts._2fa.step3 }}<br> + <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput> + <MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton> </li> </ol> - <MkInfo>{{ $ts._2fa.step4 }}</MkInfo> + <MkInfo>{{ i18n.ts._2fa.step4 }}</MkInfo> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; import { hostname } from '@/config'; import { byteify, hexify, stringify } from '@/scripts/2fa'; import MkButton from '@/components/ui/button.vue'; @@ -72,155 +72,144 @@ import MkInfo from '@/components/ui/info.vue'; import MkInput from '@/components/form/input.vue'; import MkSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, MkInfo, MkInput, MkSwitch - }, +const twoFactorData = ref<any>(null); +const supportsCredentials = ref(!!navigator.credentials); +const usePasswordLessLogin = ref($i!.usePasswordLessLogin); +const registration = ref<any>(null); +const keyName = ref(''); +const token = ref(null); - data() { - return { - data: null, - supportsCredentials: !!navigator.credentials, - usePasswordLessLogin: this.$i.usePasswordLessLogin, - registration: null, - keyName: '', - token: null, - }; - }, +function register() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/register', { + password: password + }).then(data => { + twoFactorData.value = data; + }); + }); +} - methods: { - register() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); +function unregister() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/unregister', { + password: password + }).then(() => { + usePasswordLessLogin.value = false; + updatePasswordLessLogin(); + }).then(() => { + os.success(); + $i!.twoFactorEnabled = false; + }); + }); +} + +function submit() { + os.api('i/2fa/done', { + token: token.value + }).then(() => { + os.success(); + $i!.twoFactorEnabled = true; + }).catch(e => { + os.alert({ + type: 'error', + text: e + }); + }); +} + +function registerKey() { + registration.value.saving = true; + os.api('i/2fa/key-done', { + password: registration.value.password, + name: keyName.value, + challengeId: registration.value.challengeId, + // we convert each 16 bits to a string to serialise + clientDataJSON: stringify(registration.value.credential.response.clientDataJSON), + attestationObject: hexify(registration.value.credential.response.attestationObject) + }).then(key => { + registration.value = null; + key.lastUsed = new Date(); + os.success(); + }) +} + +function unregisterKey(key) { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + return os.api('i/2fa/remove-key', { + password, + credentialId: key.id + }).then(() => { + usePasswordLessLogin.value = false; + updatePasswordLessLogin(); + }).then(() => { + os.success(); + }); + }); +} + +function addSecurityKey() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/register-key', { + password + }).then(reg => { + registration.value = { + password, + challengeId: reg!.challengeId, + stage: 0, + publicKeyOptions: { + challenge: byteify(reg!.challenge, 'base64'), + rp: { + id: hostname, + name: 'Misskey' + }, + user: { + id: byteify($i!.id, 'ascii'), + name: $i!.username, + displayName: $i!.name, + }, + pubKeyCredParams: [{ alg: -7, type: 'public-key' }], + timeout: 60000, + attestation: 'direct' + }, + saving: true + }; + return navigator.credentials.create({ + publicKey: registration.value.publicKeyOptions }); - }, + }).then(credential => { + registration.value.credential = credential; + registration.value.saving = false; + registration.value.stage = 1; + }).catch(err => { + console.warn('Error while registering?', err); + registration.value.error = err.message; + registration.value.stage = -1; + }); + }); +} - unregister() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/unregister', { - password: password - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = false; - }); - }); - }, - - submit() { - os.api('i/2fa/done', { - token: this.token - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = true; - }).catch(e => { - os.alert({ - type: 'error', - text: e - }); - }); - }, - - registerKey() { - this.registration.saving = true; - os.api('i/2fa/key-done', { - password: this.registration.password, - name: this.keyName, - challengeId: this.registration.challengeId, - // we convert each 16 bits to a string to serialise - clientDataJSON: stringify(this.registration.credential.response.clientDataJSON), - attestationObject: hexify(this.registration.credential.response.attestationObject) - }).then(key => { - this.registration = null; - key.lastUsed = new Date(); - os.success(); - }) - }, - - unregisterKey(key) { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - return os.api('i/2fa/remove-key', { - password, - credentialId: key.id - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - }); - }); - }, - - addSecurityKey() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register-key', { - password - }).then(registration => { - this.registration = { - password, - challengeId: registration.challengeId, - stage: 0, - publicKeyOptions: { - challenge: byteify(registration.challenge, 'base64'), - rp: { - id: hostname, - name: 'Misskey' - }, - user: { - id: byteify(this.$i.id, 'ascii'), - name: this.$i.username, - displayName: this.$i.name, - }, - pubKeyCredParams: [{ alg: -7, type: 'public-key' }], - timeout: 60000, - attestation: 'direct' - }, - saving: true - }; - return navigator.credentials.create({ - publicKey: this.registration.publicKeyOptions - }); - }).then(credential => { - this.registration.credential = credential; - this.registration.saving = false; - this.registration.stage = 1; - }).catch(err => { - console.warn('Error while registering?', err); - this.registration.error = err.message; - this.registration.stage = -1; - }); - }); - }, - - updatePasswordLessLogin() { - os.api('i/2fa/password-less', { - value: !!this.usePasswordLessLogin - }); - } - } -}); +async function updatePasswordLessLogin() { + await os.api('i/2fa/password-less', { + value: !!usePasswordLessLogin.value + }); +} </script>