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
	 Andreas Nedbal
					Andreas Nedbal