refactor(client): refactor 2FA settings to Composition API (#8599)
This commit is contained in:
parent
1168e25721
commit
bd620a8c77
|
@ -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,155 +72,144 @@ 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 {
|
os.inputText({
|
||||||
data: null,
|
title: i18n.ts.password,
|
||||||
supportsCredentials: !!navigator.credentials,
|
type: 'password'
|
||||||
usePasswordLessLogin: this.$i.usePasswordLessLogin,
|
}).then(({ canceled, result: password }) => {
|
||||||
registration: null,
|
if (canceled) return;
|
||||||
keyName: '',
|
os.api('i/2fa/register', {
|
||||||
token: null,
|
password: password
|
||||||
};
|
}).then(data => {
|
||||||
},
|
twoFactorData.value = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
methods: {
|
function unregister() {
|
||||||
register() {
|
os.inputText({
|
||||||
os.inputText({
|
title: i18n.ts.password,
|
||||||
title: this.$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/register', {
|
password: password
|
||||||
password: password
|
}).then(() => {
|
||||||
}).then(data => {
|
usePasswordLessLogin.value = false;
|
||||||
this.data = data;
|
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() {
|
async function updatePasswordLessLogin() {
|
||||||
os.inputText({
|
await os.api('i/2fa/password-less', {
|
||||||
title: this.$ts.password,
|
value: !!usePasswordLessLogin.value
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue