refactor(client): refactor 2FA settings to Composition API (#8599)
This commit is contained in:
		@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user