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