diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-08-28 18:25:31 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-08-28 18:25:31 +0900 |
| commit | 257c4fccf1193f111686f039e06cc4d00b9dce37 (patch) | |
| tree | b502d371495bc5a6c18349eb9fd9089cee4f4fa0 /packages/frontend/src | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.tar.gz misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.tar.bz2 misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.zip | |
feat: Refine 2fa (#11766)
* wip
* Update 2fa.qrdialog.vue
* Update 2fa.vue
* Update CHANGELOG.md
* tweak
* :v:
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/MkSignin.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/scratchpad.vue | 10 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/2fa.qrdialog.vue | 178 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/2fa.vue | 43 | ||||
| -rw-r--r-- | packages/frontend/src/style.scss | 9 |
5 files changed, 154 insertions, 88 deletions
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 2f1130d992..19f418b48d 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="ti ti-lock"></i></template> </MkInput> - <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="one-time-code" :spellcheck="false" required> + <MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required> <template #label>{{ i18n.ts.token }}</template> <template #prefix><i class="ti ti-123"></i></template> </MkInput> diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index ec251c6640..6d68a26c3c 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <div :class="$style.root"> <div :class="$style.editor" class="_panel"> - <PrismEditor v-model="code" class="_code code" :highlight="highlighter" :lineNumbers="false"/> + <PrismEditor v-model="code" class="_monospace" :class="$style.code" :highlight="highlighter" :lineNumbers="false"/> <MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton> </div> @@ -175,6 +175,14 @@ definePageMetadata({ position: relative; } +.code { + background: #2d2d2d; + color: #ccc; + font-size: 14px; + line-height: 1.5; + padding: 5px; +} + .ui { padding: 32px; } diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 245d3e79e8..cf6b0227fd 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -4,45 +4,110 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal - ref="dialogEl" - :preferType="'dialog'" - :zPriority="'low'" - @click="cancel" +<MkModalWindow + ref="dialog" + :width="500" + :height="550" @close="cancel" @closed="emit('closed')" > - <div :class="$style.root" class="_gaps_m"> - <I18n :src="i18n.ts._2fa.step1" tag="div"> - <template #a> - <a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a> + <template #header>{{ i18n.ts.setupOf2fa }}</template> + + <div style="overflow-x: clip;"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <template v-if="page === 0"> + <div style="height: 100cqh; overflow: auto; text-align: center;"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps"> + <I18n :src="i18n.ts._2fa.step1" tag="div"> + <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> + <div>{{ i18n.ts._2fa.step2 }}<br>{{ i18n.ts._2fa.step2Click }}</div> + <a :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a> + <MkKeyValue :copy="twoFactorData.url"> + <template #key>{{ i18n.ts._2fa.step2Uri }}</template> + <template #value>{{ twoFactorData.url }}</template> + </MkKeyValue> + </div> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </MkSpacer> + </div> + </template> + <template v-else-if="page === 1"> + <div style="height: 100cqh; overflow: auto;"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps"> + <div>{{ i18n.ts._2fa.step3Title }}</div> + <MkInput v-model="token" autocomplete="one-time-code" type="number"></MkInput> + <div>{{ i18n.ts._2fa.step3 }}</div> + </div> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="tokenDone">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </MkSpacer> + </div> </template> - <template #b> - <a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a> + <template v-else-if="page === 2"> + <div style="height: 100cqh; overflow: auto;"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps"> + <div style="text-align: center;">{{ i18n.ts._2fa.setupCompleted }}🎉</div> + <div style="text-align: center;">{{ i18n.ts._2fa.step4 }}</div> + <div style="text-align: center; font-weight: bold;">{{ i18n.ts._2fa.checkBackupCodesBeforeCloseThisWizard }}</div> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts._2fa.backupCodes }}</template> + + <div class="_gaps"> + <MkInfo warn>{{ i18n.ts._2fa.backupCodesDescription }}</MkInfo> + + <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> + </div> + </div> + </MkFolder> + </div> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton primary rounded gradate @click="allDone">{{ i18n.ts.done }}</MkButton> + </div> + </MkSpacer> + </div> </template> - </I18n> - <div> - {{ i18n.ts._2fa.step2 }}<br> - {{ i18n.ts._2fa.step2Click }} - </div> - <a :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a> - <MkKeyValue :copy="twoFactorData.url"> - <template #key>{{ i18n.ts._2fa.step2Url }}</template> - <template #value>{{ twoFactorData.url }}</template> - </MkKeyValue> - <div class="_buttons"> - <MkButton primary @click="ok">{{ i18n.ts.next }}</MkButton> - <MkButton @click="cancel">{{ i18n.ts.cancel }}</MkButton> - </div> + </Transition> </div> -</MkModal> +</MkModalWindow> </template> <script lang="ts" setup> +import { shallowRef, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; -import MkModal from '@/components/MkModal.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; +import MkInput from '@/components/MkInput.vue'; import { i18n } from '@/i18n'; +import * as os from '@/os'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { confetti } from '@/scripts/confetti'; defineProps<{ twoFactorData: { @@ -52,36 +117,53 @@ defineProps<{ }>(); const emit = defineEmits<{ - (ev: 'ok'): void; - (ev: 'cancel'): void; (ev: 'closed'): void; }>(); -const cancel = () => { - emit('cancel'); - emit('closed'); -}; +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const page = ref(0); +const token = ref<string | number | null>(null); +const backupCodes = ref<string[]>(); + +function cancel() { + dialog.value.close(); +} + +async function tokenDone() { + const res = await os.apiWithDialog('i/2fa/done', { + token: token.value.toString(), + }); -const ok = () => { - emit('ok'); - emit('closed'); -}; + backupCodes.value = res.backupCodes; + + page.value++; + + confetti({ + duration: 1000 * 3, + }); +} + +function allDone() { + dialog.value.close(); +} </script> <style lang="scss" module> -.root { - position: relative; - margin: auto; - padding: 32px; - min-width: 320px; - max-width: calc(100svw - 64px); - box-sizing: border-box; - background: var(--panel); - border-radius: var(--radius); +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); } .qr { - width: 20em; - max-width: 100%; + width: 200px; + max-width: 100%; } </style> diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index d2998ceae7..6871091aad 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -8,20 +8,28 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts['2fa'] }}</template> <div v-if="$i" class="_gaps_s"> - <MkFolder> + <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'partial'" warn> + {{ i18n.ts._2fa.backupCodeUsedWarning }} + </MkInfo> + <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'none'" warn> + {{ i18n.ts._2fa.backupCodesExhaustedWarning }} + </MkInfo> + + <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-shield-lock"></i></template> <template #label>{{ i18n.ts.totp }}</template> <template #caption>{{ i18n.ts.totpDescription }}</template> + <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-else @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton> + <MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton> </div> - <MkButton v-else-if="!twoFactorData && !$i.twoFactorEnabled" @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton> + <MkButton v-else-if="!$i.twoFactorEnabled" primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton> </MkFolder> <MkFolder> @@ -85,7 +93,6 @@ withDefaults(defineProps<{ first: false, }); -const twoFactorData = ref<any>(null); const supportsCredentials = ref(!!navigator.credentials); const usePasswordLessLogin = $computed(() => $i!.usePasswordLessLogin); @@ -102,31 +109,9 @@ async function registerTOTP() { password: password.result, }); - const qrdialog = await new Promise<boolean>(res => { - os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { - twoFactorData, - }, { - 'ok': () => res(true), - 'cancel': () => res(false), - }, 'closed'); - }); - if (!qrdialog) return; - - const token = await os.inputNumber({ - title: i18n.ts._2fa.step3Title, - text: i18n.ts._2fa.step3, - autocomplete: 'one-time-code', - }); - if (token.canceled) return; - - await os.apiWithDialog('i/2fa/done', { - token: token.result.toString(), - }); - - await os.alert({ - type: 'success', - text: i18n.ts._2fa.step4, - }); + os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { + twoFactorData, + }, {}, 'closed'); } function unregisterTOTP() { diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 948d27536c..c644fc76da 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -400,15 +400,6 @@ hr { font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; } -._code { - @extend ._monospace; - background: #2d2d2d; - color: #ccc; - font-size: 14px; - line-height: 1.5; - padding: 5px; -} - .prism-editor__textarea:focus { outline: none; } |