mizzkey/src/client/pages/settings/security.2fa.vue
syuilo 7199e6f4e0
Migrate to Vue3 (#6587)
* Update reaction.vue

* fix  bug

* wip

* wip

* wjio

* wip

* Revert "wip"

This reverts commit e427f2160a.

* wip

* wip

* wip

* Update init.ts

* Update drive-window.vue

* wip

* wip

* Use PascalCase for components

* Use PascalCase for components

* update dep

* wip

* wip

* wip

* Update init.ts

* wip

* Update paging.ts

* Update test.vue

* watch deep

* wip

* lint

* wip

* wip

* wip

* wip

* wiop

* wip

* Update webpack.config.ts

* alllow null poll

* wip

* wip

* wip

* wiop

* UI redesign & refactor (#6714)

* wip

* wip

* wip

* wip

* wip

* Update drive.vue

* Update word-mute.vue

* wip

* wip

* wip

* clean up

* wip

* Update default.vue

* wip

* Update notes.vue

* Update mfm.ts

* Update index.home.vue

* Update post-form.vue

* Update post-form-attaches.vue

* wip

* Update post-form.vue

* Update sidebar.vue

* wip

* wip

* Update index.vue

* wip

* Update default.vue

* Update index.vue

* Update index.vue

* wip

* Update post-form-attaches.vue

* Update note.vue

* wip

* clean up

* Update notes.vue

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* Update index.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update default.vue

* wip

* Update _dark.json5

* wip

* wip

* wip

* clean up

* wip

* wip

* Update index.vue

* Update test.vue

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* clena yop

* wip

* wip

* Update store.ts

* Update messaging-room.vue

* Update default.widgets.vue

* fix

* wip

* wip

* Update modal.vue

* wip

* Update os.ts

* Update os.ts

* Update deck.vue

* Update init.ts

* wip

* Update ja-JP.yml

* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない

* Update modal.vue

* wip

* Update tooltip.ts

* wip

* wip

* wip

* wip

* wip

* Update image-viewer.vue

* wip

* wip

* Update style.scss

* Update style.scss

* Update visitor.vue

* wip

* Update init.ts

* Update init.ts

* wip

* wip

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* wip

* wip

* Update modal.vue

* Update header.vue

* Update menu.vue

* Update about.vue

* Update about-misskey.vue

* wip

* wip

* Update visitor.vue

* Update tooltip.ts

* wip

* Update drive.vue

* wip

* Update style.scss

* Update header.vue

* wip

* wip

* Update users.user.vue

* Update announcements.vue

* wip

* wip

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update style.scss

* Update users.vue

* wip

* Update style.scss

* wip

* Update welcome.entrance.vue

* Update radio.vue

* Update size.ts

* Update emoji-edit-dialog.vue

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* wip

* wip

* wip

* wip

* Update file-dialog.vue

* wip

* wip

* Update token-generate-window.vue

* Update notification-setting-window.vue

* wip

* wip

* Update _error_.vue

* Update ja-JP.yml

* wip

* wip

* Update store.ts

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* Update announcements.vue

* Update store.ts

* wip

* Update page-editor.vue

* wip

* wip

* Update modal.vue

* wip

* Update select-file.ts

* Update timeline.vue

* Update emojis.vue

* Update os.ts

* wip

* Update user-select.vue

* Update mfm.ts

* Update get-file-info.ts

* Update drive.vue

* Update init.ts

* Update mfm.ts

* wip

* wip

* Update window.vue

* Update note.vue

* wip

* wip

* Update user-info.vue

* wip

* wip

* wip

* wip

* wip

* Update header.vue

* Update header.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update webpack.config.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update autocomplete.ts

* wip

* wip

* wip

* Update toast.vue

* wip

* Update post-form-dialog.vue

* wip

* wip

* wip

* wip

* wip

* Update users.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update package.json

* wip

* Update icon-dialog.vue

* wip

* wip

* Update user-preview.ts

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* Update user-name.vue

* Update federation.vue

* Update instance.vue

* wip

* wip

* Update tag.vue

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* wip

* Update os.ts

* Update os.ts

* wip

* wip

* wip

* Update router.ts

* wip

* Update init.ts

* Update note.vue

* Update messages.vue

* wip

* wip

* wip

* wip

* wip

* google

* wip

* wip

* wip

* wip

* Update theme-editor.vue

* wip

* wip

* Update room.vue

* Update channel-editor.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update menu.vue

* wip

* wip

* wip

* wip

* Update messaging-room.vue

* wip

* Update post-form.vue

* Update default.widgets.vue

* Update window.vue

* wip
2020-10-17 20:12:00 +09:00

236 lines
6.9 KiB
Vue

<template>
<section class="_card">
<div class="_title"><Fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
<div class="_content">
<MkButton v-if="!data && !$store.state.i.twoFactorEnabled" @click="register">{{ $t('_2fa.registerDevice') }}</MkButton>
<template v-if="$store.state.i.twoFactorEnabled">
<p>{{ $t('_2fa.alreadyRegistered') }}</p>
<MkButton @click="unregister">{{ $t('unregister') }}</MkButton>
<template v-if="supportsCredentials">
<hr class="totp-method-sep">
<h2 class="heading">{{ $t('securityKey') }}</h2>
<p>{{ $t('_2fa.securityKeyInfo') }}</p>
<div class="key-list">
<div class="key" v-for="key in $store.state.i.securityKeysList">
<h3>{{ key.name }}</h3>
<div class="last-used">{{ $t('lastUsed') }}<MkTime :time="key.lastUsed"/></div>
<MkButton @click="unregisterKey(key)">{{ $t('unregister') }}</MkButton>
</div>
</div>
<MkSwitch v-model:value="usePasswordLessLogin" @update:value="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</MkSwitch>
<MkInfo warn v-if="registration && registration.error">{{ $t('error') }} {{ registration.error }}</MkInfo>
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</MkButton>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
{{ $t('tapSecurityKey') }}
<Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
</li>
<li v-if="registration.stage >= 1">
<MkForm :disabled="registration.stage != 1 || registration.saving">
<MkInput v-model:value="keyName" :max="30">
<span>{{ $t('securityKeyName') }}</span>
</MkInput>
<MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $t('registerSecurityKey') }}</MkButton>
<Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
</MkForm>
</li>
</ol>
</template>
</template>
<div v-if="data && !$store.state.i.twoFactorEnabled">
<ol style="margin: 0; padding: 0 0 0 1em;">
<li>
<i18n-t keypath="_2fa.step1" tag="span">
<template #a>
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
</template>
<template #b>
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
</template>
</i18n-t>
</li>
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
<li>{{ $t('_2fa.step3') }}<br>
<MkInput v-model:value="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</MkInput>
<MkButton primary @click="submit">{{ $t('done') }}</MkButton>
</li>
</ol>
<MkInfo>{{ $t('_2fa.step4') }}</MkInfo>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { hostname } from '@/config';
import { byteify, hexify, stringify } from '@/scripts/2fa';
import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton, MkInfo, MkInput, MkSwitch
},
data() {
return {
data: null,
supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
registration: null,
keyName: '',
token: null,
faLock
};
},
methods: {
register() {
os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register', {
password: password
}).then(data => {
this.data = data;
});
});
},
unregister() {
os.dialog({
title: this.$t('password'),
input: {
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.$store.state.i.twoFactorEnabled = false;
});
});
},
submit() {
os.api('i/2fa/done', {
token: this.token
}).then(() => {
os.success();
this.$store.state.i.twoFactorEnabled = true;
}).catch(e => {
os.dialog({
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.dialog({
title: this.$t('password'),
input: {
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.dialog({
title: this.$t('password'),
input: {
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.$store.state.i.id, 'ascii'),
name: this.$store.state.i.username,
displayName: this.$store.state.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>