refactor(client): refactor 2FA settings to Composition API (#8599)

This commit is contained in:
Andreas Nedbal 2022-05-05 15:41:10 +02:00 committed by GitHub
parent 1168e25721
commit bd620a8c77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 157 additions and 168 deletions

View File

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