enhance(frontend/2fa): 二要素認証のバックアップコードを保存するように促す (MisskeyIO#611)
This commit is contained in:
parent
7da775df10
commit
1f38f58117
@ -2014,9 +2014,12 @@ _2fa:
|
||||
renewTOTPCancel: "Cancel"
|
||||
checkBackupCodesBeforeCloseThisWizard: "Before you close this window, please note the following backup codes."
|
||||
backupCodes: "Backup codes"
|
||||
backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place."
|
||||
backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it."
|
||||
backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification."
|
||||
backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authenticator app. Each can only be used once. Please keep them in a safe place."
|
||||
backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentication as soon as possible if you are no longer able to use it."
|
||||
backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentication app, you will be unable to access this account. Please reconfigure two-factor authentication."
|
||||
backupCodesSavedConfirmTitle: "Did you save your backup codes?"
|
||||
backupCodesSavedConfirmDescription: "If you lose both your two-factor authentication app and backup codes, YOU WILL LOSE ACCESS TO YOUR ACCOUNT.\nKeep them safe and secure, and do not share them with anyone.\n\n$[x2 Two-factor authentication settings CANNOT be changed by anyone other than yourself, $[fg.color=red AND THE ADMINISTRATOR CANNOT DISABLE IT EITHER.]]"
|
||||
backupCodesSavedConfirmChecked: "I have saved my backup codes"
|
||||
howto2fa: "If you are having trouble setting up, please refer to {link}."
|
||||
_permissions:
|
||||
"read:account": "View your account information"
|
||||
|
15
locales/index.d.ts
vendored
15
locales/index.d.ts
vendored
@ -7856,6 +7856,21 @@ export interface Locale extends ILocale {
|
||||
* バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。
|
||||
*/
|
||||
"backupCodesExhaustedWarning": string;
|
||||
/**
|
||||
* バックアップコードを保存しましたか?
|
||||
*/
|
||||
"backupCodesSavedConfirmTitle": string;
|
||||
/**
|
||||
* 二要素認証アプリとバックアップコードの両方を紛失した場合、アカウントにアクセスできなくなります。
|
||||
* 誰とも共有せず、適切な方法で保管してください。
|
||||
*
|
||||
* $[x2 二要素認証設定は自分以外の誰にも変更できませんので、$[fg.color=red 運営チームも無効化することはできません。]]
|
||||
*/
|
||||
"backupCodesSavedConfirmDescription": string;
|
||||
/**
|
||||
* バックアップコードを保存しました
|
||||
*/
|
||||
"backupCodesSavedConfirmChecked": string;
|
||||
/**
|
||||
* 設定方法でお困りの際は、{link}を参照してください。
|
||||
*/
|
||||
|
@ -2064,6 +2064,9 @@ _2fa:
|
||||
backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。"
|
||||
backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。"
|
||||
backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。"
|
||||
backupCodesSavedConfirmTitle: "バックアップコードを保存しましたか?"
|
||||
backupCodesSavedConfirmDescription: "二要素認証アプリとバックアップコードの両方を紛失した場合、アカウントにアクセスできなくなります。\n誰とも共有せず、適切な方法で保管してください。\n\n$[x2 二要素認証設定は自分以外の誰にも変更できませんので、$[fg.color=red 運営チームも無効化することはできません。]]"
|
||||
backupCodesSavedConfirmChecked: "バックアップコードを保存しました"
|
||||
howto2fa: "設定方法でお困りの際は、{link}を参照してください。"
|
||||
|
||||
_permissions:
|
||||
|
@ -1992,9 +1992,12 @@ _2fa:
|
||||
renewTOTPCancel: "취소"
|
||||
checkBackupCodesBeforeCloseThisWizard: "이 위자드를 닫기 전에 아래 백업 코드를 확인하십시오"
|
||||
backupCodes: "백업 코드"
|
||||
backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
|
||||
backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
|
||||
backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
|
||||
backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다. 이 코드들은 반드시 안전한 장소에 보관하십시오. 각 코드는 한 번만 사용할 수 있습니다."
|
||||
backupCodeUsedWarning: "백업 코드가 사용되었습니다. 인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
|
||||
backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다. 인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다. 인증 앱을 다시 등록해 주세요."
|
||||
backupCodesSavedConfirmTitle: "백업 코드를 저장했습니까?"
|
||||
backupCodesSavedConfirmDescription: "인증 앱과 백업 코드를 모두 분실하면\n계정에 액세스할 수 없게 됩니다.\n자신만이 알 수 있도록 안전한 장소에 보관해 주십시오.\n\n$[x2 2단계 인증 설정은\n본인만이 변경할 수 있으며, $[fg.color=red 운영팀도 해제할 수 없습니다.]]"
|
||||
backupCodesSavedConfirmChecked: "백업 코드를 저장했습니다"
|
||||
howto2fa: "설정 방법에 대한 자세한 내용은 {link}를 참조하세요."
|
||||
_permissions:
|
||||
"read:account": "계정의 정보를 봅니다"
|
||||
|
@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
</MkSelect>
|
||||
<MkSwitch v-if="switchLabel" v-model="switchValue" style="display: flex; margin: 1em 0; justify-content: center;">{{ switchLabel }}</MkSwitch>
|
||||
<details v-if="details" class="_acrylic" style="margin: 1em 0;">
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<div class="_gaps_s" style="text-align: initial;">
|
||||
@ -58,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</details>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okDisabled || okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}<span v-if="okDisabled && okWaitInitiated"> ({{ sec }})</span></MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
@ -69,11 +70,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, computed, watch } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@ -104,6 +106,7 @@ const props = withDefaults(defineProps<{
|
||||
text?: string | null;
|
||||
input?: Input;
|
||||
select?: Select;
|
||||
switchLabel?: string | null;
|
||||
details?: Record<string, string>;
|
||||
actions?: {
|
||||
text: string;
|
||||
@ -115,6 +118,8 @@ const props = withDefaults(defineProps<{
|
||||
showCancelButton?: boolean;
|
||||
cancelableByBgClick?: boolean;
|
||||
okText?: string;
|
||||
okWaitInitiate?: 'dialog' | 'input' | 'switch';
|
||||
okWaitDuration?: number;
|
||||
cancelText?: string;
|
||||
}>(), {
|
||||
type: 'info',
|
||||
@ -123,17 +128,20 @@ const props = withDefaults(defineProps<{
|
||||
text: undefined,
|
||||
input: undefined,
|
||||
select: undefined,
|
||||
switchLabel: undefined,
|
||||
details: undefined,
|
||||
actions: undefined,
|
||||
showOkButton: true,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: true,
|
||||
okText: undefined,
|
||||
okWaitInitiate: undefined,
|
||||
okWaitDuration: 0,
|
||||
cancelText: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
|
||||
(ev: 'done', v: { canceled: true } | { canceled: false, result: Result, toggle: boolean }): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
@ -141,6 +149,16 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||
const selectedValue = ref(props.select?.default ?? null);
|
||||
const switchValue = ref<boolean>(false);
|
||||
|
||||
const sec = ref(props.okWaitDuration);
|
||||
const okWaitInitiated = computed(() => {
|
||||
if (props.okWaitInitiate === 'dialog') return true;
|
||||
if (props.okWaitInitiate === 'input') return inputValue.value !== null;
|
||||
if (props.okWaitInitiate === 'switch') return switchValue.value;
|
||||
return false;
|
||||
});
|
||||
const okDisabled = computed(() => sec.value > 0);
|
||||
|
||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||
if (props.input) {
|
||||
@ -161,9 +179,9 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
||||
|
||||
// overload function を使いたいので lint エラーを無視する
|
||||
function done(canceled: true): void;
|
||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
|
||||
emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
|
||||
function done(canceled: false, result: Result, toggle: boolean): void; // eslint-disable-line no-redeclare
|
||||
function done(canceled: boolean, result?: Result, toggle?: boolean ): void { // eslint-disable-line no-redeclare
|
||||
emit('done', { canceled, result, toggle } as { canceled: true } | { canceled: false, result: Result, toggle: boolean });
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
@ -174,18 +192,13 @@ async function ok() {
|
||||
props.input ? inputValue.value :
|
||||
props.select ? selectedValue.value :
|
||||
true;
|
||||
done(false, result);
|
||||
done(false, result, switchValue.value);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
done(true);
|
||||
}
|
||||
|
||||
/*
|
||||
function onBgClick() {
|
||||
if (props.cancelableByBgClick) cancel();
|
||||
}
|
||||
*/
|
||||
function onKeydown(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Escape') cancel();
|
||||
}
|
||||
@ -198,8 +211,24 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(okWaitInitiated, () => {
|
||||
sec.value = props.okWaitDuration;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
|
||||
sec.value = props.okWaitDuration;
|
||||
if (sec.value > 0) {
|
||||
const waitTimer = setInterval(() => {
|
||||
if (!okWaitInitiated.value) return;
|
||||
|
||||
if (sec.value < 0) {
|
||||
clearInterval(waitTimer);
|
||||
}
|
||||
sec.value = sec.value - 1;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -221,7 +221,10 @@ export function alert(props: {
|
||||
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
switchLabel?: string | null;
|
||||
details?: Record<string, string>;
|
||||
okWaitInitiate?: 'dialog' | 'input' | 'switch';
|
||||
okWaitDuration?: number;
|
||||
}): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
popup(MkDialog, props, {
|
||||
@ -236,8 +239,11 @@ export function confirm(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
switchLabel?: string | null;
|
||||
details?: Record<string, string>;
|
||||
okText?: string;
|
||||
okWaitInitiate?: 'dialog' | 'input' | 'switch';
|
||||
okWaitDuration?: number;
|
||||
cancelText?: string;
|
||||
}): Promise<{ canceled: boolean }> {
|
||||
return new Promise(resolve => {
|
||||
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="500"
|
||||
:height="550"
|
||||
:height="600"
|
||||
@close="cancel"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
@ -76,15 +76,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div class="_gaps">
|
||||
<MkInfo warn>{{ i18n.ts._2fa.backupCodesDescription }}</MkInfo>
|
||||
<MkButton primary rounded gradate full @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
|
||||
|
||||
<div v-for="(code, i) in backupCodes" :key="code" class="_gaps_s">
|
||||
<MkKeyValue :copy="code">
|
||||
<template #key>#{{ i + 1 }}</template>
|
||||
<template #value><code class="_monospace">{{ code }}</code></template>
|
||||
</MkKeyValue>
|
||||
<span style="text-align: center;">#{{ i + 1 }}. <code class="_monospace">{{ code }}</code></span>
|
||||
</div>
|
||||
|
||||
<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
@ -107,6 +103,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import * as config from '@/config.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { confetti } from '@/scripts/confetti.js';
|
||||
@ -131,7 +128,8 @@ const token = ref<string | number | null>(null);
|
||||
const backupCodes = ref<string[]>();
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
if (page.value !== 2) dialog.value?.close();
|
||||
else allDone();
|
||||
}
|
||||
|
||||
async function tokenDone() {
|
||||
@ -150,15 +148,27 @@ async function tokenDone() {
|
||||
|
||||
function downloadBackupCodes() {
|
||||
if (backupCodes.value !== undefined) {
|
||||
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
|
||||
const txtBlob = new Blob([backupCodes.value.reduce((acc, code, i) => `${acc}#${i + 1}. ${code}\r\n`, `${config.hostname} 2FA Backup Codes\r\n\r\n`)], { type: 'text/plain' });
|
||||
const dummya = document.createElement('a');
|
||||
dummya.href = URL.createObjectURL(txtBlob);
|
||||
dummya.download = `${$i.username}-2fa-backup-codes.txt`;
|
||||
dummya.download = `${config.hostname}-${$i.username}-2fa-backup-codes.txt`;
|
||||
dummya.click();
|
||||
}
|
||||
}
|
||||
|
||||
function allDone() {
|
||||
async function allDone() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._2fa.backupCodesSavedConfirmTitle,
|
||||
text: i18n.ts._2fa.backupCodesSavedConfirmDescription,
|
||||
switchLabel: i18n.ts._2fa.backupCodesSavedConfirmChecked,
|
||||
okText: i18n.ts.gotIt,
|
||||
okWaitInitiate: 'switch',
|
||||
okWaitDuration: 5,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
</script>
|
||||
|
@ -25,13 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInfo>
|
||||
<Mfm :text="i18n.tsx._2fa.howto2fa({ link: `[${i18n.ts.here}](https://go.misskey.io/howto-2fa)`})"/>
|
||||
</MkInfo>
|
||||
<MkInfo v-if="$i.securityKeysList.length > 0">{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
||||
|
||||
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
|
||||
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
|
||||
<template v-if="$i.securityKeysList.length > 0">
|
||||
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
||||
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
||||
</template>
|
||||
<MkButton v-if="$i.securityKeysList.length > 0" @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
||||
<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user